From 1627049348ff47a03c533cc4c16155e5e91cefdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Thu, 17 Jul 2025 09:49:03 +0200 Subject: [PATCH 1/5] fix stripe tokens in production --- build.sbt | 2 +- .../payment/message/PaymentMessages.scala | 44 +- .../payment/spi/PaymentAccountApi.scala | 59 +- .../persistence/typed/PaymentBehavior.scala | 18 +- .../service/BankAccountEndpoints.scala | 3 +- .../payment/service/CheckoutEndpoints.scala | 2 +- .../payment/service/PaymentService.scala | 27 +- .../service/UboDeclarationEndpoints.scala | 16 +- .../payment/spi/MangoPayProvider.scala | 38 +- .../payment/spi/StripeAccountApi.scala | 579 +++++++++--------- .../payment/scalatest/PaymentRouteSpec.scala | 24 +- .../scalatest/StripePaymentRouteTestKit.scala | 2 +- .../scalatest/StripePaymentTestKit.scala | 141 +++++ .../payment/spi/MockMangoPayProvider.scala | 38 +- .../payment/handlers/PaymentHandlerSpec.scala | 7 +- .../service/StripePaymentServiceSpec.scala | 81 ++- 16 files changed, 722 insertions(+), 359 deletions(-) diff --git a/build.sbt b/build.sbt index 2a79b5e..a213b80 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ ThisBuild / organization := "app.softnetwork" name := "payment" -ThisBuild / version := "0.7.3.2" +ThisBuild / version := "0.7.4" ThisBuild / scalaVersion := "2.12.18" diff --git a/common/src/main/scala/app/softnetwork/payment/message/PaymentMessages.scala b/common/src/main/scala/app/softnetwork/payment/message/PaymentMessages.scala index 3001edc..79b9e31 100644 --- a/common/src/main/scala/app/softnetwork/payment/message/PaymentMessages.scala +++ b/common/src/main/scala/app/softnetwork/payment/message/PaymentMessages.scala @@ -84,7 +84,7 @@ object PaymentMessages { * @param paymentMethodId * - optional payment method id * @param registerMeansOfPayment - * - optional flag to specify whether or not the means of payment should be registered + * - optional flag to specify whether the means of payment should be registered */ case class Payment( orderUuid: String, @@ -140,7 +140,7 @@ object PaymentMessages { * @param paymentMethodId * - optional payment method id * @param registerMeansOfPayment - * - optional flag to specify whether or not the means of payment should be registered + * - optional flag to specify whether the means of payment should be registered * @param clientId * - optional client id */ @@ -644,7 +644,9 @@ object PaymentMessages { case class BankAccountCommand( bankAccount: BankAccount, user: Either[NaturalUser, LegalUser], - acceptedTermsOfPSP: Option[Boolean] = None + acceptedTermsOfPSP: Option[Boolean] = None, + tokenId: Option[String] = None, + bankTokenId: Option[String] = None ) object BankAccountCommand { @@ -652,14 +654,20 @@ object PaymentMessages { def apply( bankAccount: BankAccount, naturalUser: NaturalUser, - acceptedTermsOfPSP: Option[Boolean] - ): BankAccountCommand = BankAccountCommand(bankAccount, Left(naturalUser), acceptedTermsOfPSP) + acceptedTermsOfPSP: Option[Boolean], + tokenId: Option[String], + bankTokenId: Option[String] + ): BankAccountCommand = + BankAccountCommand(bankAccount, Left(naturalUser), acceptedTermsOfPSP, tokenId, bankTokenId) def apply( bankAccount: BankAccount, legalUser: LegalUser, - acceptedTermsOfPSP: Option[Boolean] - ): BankAccountCommand = BankAccountCommand(bankAccount, Right(legalUser), acceptedTermsOfPSP) + acceptedTermsOfPSP: Option[Boolean], + tokenId: Option[String], + bankTokenId: Option[String] + ): BankAccountCommand = + BankAccountCommand(bankAccount, Right(legalUser), acceptedTermsOfPSP, tokenId, bankTokenId) } /** @param creditedAccount @@ -676,6 +684,10 @@ object PaymentMessages { * - user agent * @param clientId * - optional client id + * @param tokenId + * - optional account token id + * @param bankTokenId + * - optional bank token id */ case class CreateOrUpdateBankAccount( creditedAccount: String, @@ -684,7 +696,9 @@ object PaymentMessages { acceptedTermsOfPSP: Option[Boolean] = None, ipAddress: Option[String] = None, userAgent: Option[String], - clientId: Option[String] = None + clientId: Option[String] = None, + tokenId: Option[String] = None, + bankTokenId: Option[String] = None ) extends PaymentCommandWithKey with PaymentAccountCommand { val key: String = creditedAccount @@ -805,9 +819,19 @@ object PaymentMessages { /** @param creditedAccount * - account which owns the UBO declaration that would be validated + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the user */ - case class ValidateUboDeclaration(creditedAccount: String, ipAddress: String, userAgent: String) - extends PaymentCommandWithKey + case class ValidateUboDeclaration( + creditedAccount: String, + ipAddress: String, + userAgent: String, + tokenId: Option[String] = None + ) extends PaymentCommandWithKey with PaymentAccountCommand { val key: String = creditedAccount } diff --git a/common/src/main/scala/app/softnetwork/payment/spi/PaymentAccountApi.scala b/common/src/main/scala/app/softnetwork/payment/spi/PaymentAccountApi.scala index 9f9d6be..d57dfdf 100644 --- a/common/src/main/scala/app/softnetwork/payment/spi/PaymentAccountApi.scala +++ b/common/src/main/scala/app/softnetwork/payment/spi/PaymentAccountApi.scala @@ -15,6 +15,14 @@ trait PaymentAccountApi { _: PaymentContext => /** @param maybePaymentAccount * - payment account to create or update + * @param acceptedTermsOfPSP + * - whether the user has accepted the terms of the PSP + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the payment account * @return * provider user id */ @@ -22,15 +30,22 @@ trait PaymentAccountApi { _: PaymentContext => maybePaymentAccount: Option[PaymentAccount], acceptedTermsOfPSP: Boolean, ipAddress: Option[String], - userAgent: Option[String] + userAgent: Option[String], + tokenId: Option[String] = None ): Option[String] = { maybePaymentAccount match { case Some(paymentAccount) => import paymentAccount._ if (user.isLegalUser) { - createOrUpdateLegalUser(user.legalUser, acceptedTermsOfPSP, ipAddress, userAgent) + createOrUpdateLegalUser(user.legalUser, acceptedTermsOfPSP, ipAddress, userAgent, tokenId) } else if (user.isNaturalUser) { - createOrUpdateNaturalUser(user.naturalUser, acceptedTermsOfPSP, ipAddress, userAgent) + createOrUpdateNaturalUser( + user.naturalUser, + acceptedTermsOfPSP, + ipAddress, + userAgent, + tokenId + ) } else { None } @@ -40,6 +55,14 @@ trait PaymentAccountApi { _: PaymentContext => /** @param maybeNaturalUser * - natural user to create + * @param acceptedTermsOfPSP + * - whether the user has accepted the terms of the PSP + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the payment account * @return * provider user id */ @@ -48,11 +71,20 @@ trait PaymentAccountApi { _: PaymentContext => maybeNaturalUser: Option[NaturalUser], acceptedTermsOfPSP: Boolean, ipAddress: Option[String], - userAgent: Option[String] + userAgent: Option[String], + tokenId: Option[String] ): Option[String] /** @param maybeLegalUser * - legal user to create or update + * @param acceptedTermsOfPSP + * - whether the user has accepted the terms of the PSP + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the payment account * @return * provider user id */ @@ -61,7 +93,8 @@ trait PaymentAccountApi { _: PaymentContext => maybeLegalUser: Option[LegalUser], acceptedTermsOfPSP: Boolean, ipAddress: Option[String], - userAgent: Option[String] + userAgent: Option[String], + tokenId: Option[String] ): Option[String] /** @param userId @@ -99,6 +132,12 @@ trait PaymentAccountApi { _: PaymentContext => * - Provider user id * @param uboDeclarationId * - Provider declaration id + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the payment account * @return * Ultimate Beneficial Owner declaration */ @@ -106,7 +145,8 @@ trait PaymentAccountApi { _: PaymentContext => userId: String, uboDeclarationId: String, ipAddress: String, - userAgent: String + userAgent: String, + tokenId: Option[String] ): Option[UboDeclaration] /** @param userId @@ -144,10 +184,15 @@ trait PaymentAccountApi { _: PaymentContext => /** @param maybeBankAccount * - bank account to create + * @param bankTokenId + * - optional bank token id for the payment account * @return * bank account id */ - def createOrUpdateBankAccount(maybeBankAccount: Option[BankAccount]): Option[String] + def createOrUpdateBankAccount( + maybeBankAccount: Option[BankAccount], + bankTokenId: Option[String] + ): Option[String] /** @param userId * - provider user id diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala index 0d4b92d..495154c 100644 --- a/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala +++ b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala @@ -1181,7 +1181,8 @@ trait PaymentBehavior Some(updatedPaymentAccount), acceptedTermsOfPSP.getOrElse(false), ipAddress, - userAgent + userAgent, + tokenId ) case Some(_) if shouldUpdateUser => if (shouldUpdateUserType) { @@ -1189,14 +1190,16 @@ trait PaymentBehavior Some(updatedPaymentAccount.resetUserId(None)), acceptedTermsOfPSP.getOrElse(false), ipAddress, - userAgent + userAgent, + tokenId ) } else { createOrUpdatePaymentAccount( Some(updatedPaymentAccount), acceptedTermsOfPSP.getOrElse(false), ipAddress, - userAgent + userAgent, + tokenId ) } case some => some @@ -1228,11 +1231,13 @@ trait PaymentBehavior (paymentAccount.bankAccount.flatMap(_.id) match { case None => createOrUpdateBankAccount( - updatedPaymentAccount.resetBankAccountId().bankAccount + updatedPaymentAccount.resetBankAccountId().bankAccount, + bankTokenId ) case Some(_) if shouldCreateOrUpdateBankAccount => createOrUpdateBankAccount( - updatedPaymentAccount.resetBankAccountId().bankAccount + updatedPaymentAccount.resetBankAccountId().bankAccount, + bankTokenId ) case some => some }) match { @@ -1696,7 +1701,8 @@ trait PaymentBehavior paymentAccount.userId.getOrElse(""), uboDeclaration.id, ipAddress, - userAgent + userAgent, + tokenId ) match { case Some(declaration) => val updatedUbo = declaration.withUbos(uboDeclaration.ubos) diff --git a/core/src/main/scala/app/softnetwork/payment/service/BankAccountEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/BankAccountEndpoints.scala index 083ffc9..0ea1cc1 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/BankAccountEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/BankAccountEndpoints.scala @@ -76,7 +76,8 @@ trait BankAccountEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { acceptedTermsOfPSP, clientId = client.map(_.clientId).orElse(session.clientId), ipAddress = ipAddress, - userAgent = userAgent + userAgent = userAgent, + tokenId = tokenId ) ).map { case r: BankAccountCreatedOrUpdated => Right(r) diff --git a/core/src/main/scala/app/softnetwork/payment/service/CheckoutEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/CheckoutEndpoints.scala index a9bac68..c18e55e 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/CheckoutEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/CheckoutEndpoints.scala @@ -92,7 +92,7 @@ trait CheckoutEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { printReceipt, creditedAccount.headOption, feesAmount, - user = user, // required for Pre authorize without pre registered card + user = user, // required for Pre authorize without preregistered card paymentMethodId = paymentMethodId, registerMeansOfPayment = registerMeansOfPayment ) diff --git a/core/src/main/scala/app/softnetwork/payment/service/PaymentService.scala b/core/src/main/scala/app/softnetwork/payment/service/PaymentService.scala index abc89b3..ef29885 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/PaymentService.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/PaymentService.scala @@ -588,7 +588,9 @@ trait PaymentService[SD <: SessionData with SessionDataDecorator[SD]] updatedUser, acceptedTermsOfPSP, Some(ipAddress.value), - userAgent.map(_.name()) + userAgent.map(_.name()), + tokenId = tokenId, + bankTokenId = bankTokenId ) ) completeWith { case r: BankAccountCreatedOrUpdated => @@ -633,16 +635,19 @@ trait PaymentService[SD <: SessionData with SessionDataDecorator[SD]] } ~ put { optionalHeaderValueByType[UserAgent]((): Unit) { userAgent => extractClientIP { ipAddress => - run( - ValidateUboDeclaration( - externalUuidWithProfile(session), - ipAddress.value, - userAgent.map(_.name()) - ) - ) completeWith { - case _: UboDeclarationAskedForValidation.type => - complete(HttpResponse(StatusCodes.OK)) - case other => error(other) + parameter("tokenId".?) { tokenId => + run( + ValidateUboDeclaration( + externalUuidWithProfile(session), + ipAddress.value, + userAgent.map(_.name()), + tokenId + ) + ) completeWith { + case _: UboDeclarationAskedForValidation.type => + complete(HttpResponse(StatusCodes.OK)) + case other => error(other) + } } } } diff --git a/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala index 24431f7..9a7e3ce 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala @@ -63,6 +63,11 @@ trait UboDeclarationEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { requiredSessionEndpoint.put .in(clientIp) .in(header[Option[String]](HeaderNames.UserAgent)) + .in( + query[Option[String]]("tokenId").description( + "The token id of the Ubo declaration to validate" + ) + ) .in(PaymentSettings.PaymentConfig.declarationRoute) .out( statusCode(StatusCode.Ok).and( @@ -71,8 +76,15 @@ trait UboDeclarationEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { ) ) ) - .serverLogic(principal => { case (ipAddress, userAgent) => - run(ValidateUboDeclaration(externalUuidWithProfile(principal._2), ipAddress, userAgent)) + .serverLogic(principal => { case (ipAddress, userAgent, tokenId) => + run( + ValidateUboDeclaration( + externalUuidWithProfile(principal._2), + ipAddress, + userAgent, + tokenId + ) + ) .map { case UboDeclarationAskedForValidation => Right(UboDeclarationAskedForValidation) case other => Left(error(other)) diff --git a/mangopay/src/main/scala/app/softnetwork/payment/spi/MangoPayProvider.scala b/mangopay/src/main/scala/app/softnetwork/payment/spi/MangoPayProvider.scala index 88d395f..6f23004 100644 --- a/mangopay/src/main/scala/app/softnetwork/payment/spi/MangoPayProvider.scala +++ b/mangopay/src/main/scala/app/softnetwork/payment/spi/MangoPayProvider.scala @@ -216,6 +216,14 @@ trait MangoPayProvider extends PaymentProvider { /** @param maybeNaturalUser * - natural user to create + * @param acceptedTermsOfPSP + * - whether the user has accepted the terms of the PSP + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the payment account * @return * provider user id */ @@ -224,7 +232,8 @@ trait MangoPayProvider extends PaymentProvider { maybeNaturalUser: Option[NaturalUser], acceptedTermsOfPSP: Boolean, ipAddress: Option[String], - userAgent: Option[String] + userAgent: Option[String], + tokenId: Option[String] ): Option[String] = { maybeNaturalUser match { case Some(naturalUser) => @@ -287,6 +296,14 @@ trait MangoPayProvider extends PaymentProvider { /** @param maybeLegalUser * - legal user to create + * @param acceptedTermsOfPSP + * - whether the user has accepted the terms of the PSP + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the payment account * @return * provider user id */ @@ -295,7 +312,8 @@ trait MangoPayProvider extends PaymentProvider { maybeLegalUser: Option[LegalUser], acceptedTermsOfPSP: Boolean, ipAddress: Option[String], - userAgent: Option[String] + userAgent: Option[String], + tokenId: Option[String] ): Option[String] = { maybeLegalUser match { case Some(legalUser) => @@ -441,10 +459,15 @@ trait MangoPayProvider extends PaymentProvider { /** @param maybeBankAccount * - bank account to create + * @param bankTokenId + * - optional bank token id for the bank account * @return * bank account id */ - def createOrUpdateBankAccount(maybeBankAccount: Option[BankAccount]): Option[String] = { + def createOrUpdateBankAccount( + maybeBankAccount: Option[BankAccount], + bankTokenId: Option[String] + ): Option[String] = { maybeBankAccount match { case Some(mangoPayBankAccount) => import mangoPayBankAccount._ @@ -2309,6 +2332,12 @@ trait MangoPayProvider extends PaymentProvider { * - Provider user id * @param uboDeclarationId * - Provider declaration id + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the payment account * @return * Ultimate Beneficial Owner declaration */ @@ -2316,7 +2345,8 @@ trait MangoPayProvider extends PaymentProvider { userId: String, uboDeclarationId: String, ipAddress: String, - userAgent: String + userAgent: String, + tokenId: Option[String] ): Option[UboDeclaration] = { Try( MangoPay().getUboDeclarationApi.submitForValidation(userId, uboDeclarationId) diff --git a/stripe/src/main/scala/app/softnetwork/payment/spi/StripeAccountApi.scala b/stripe/src/main/scala/app/softnetwork/payment/spi/StripeAccountApi.scala index 01b5346..264434e 100644 --- a/stripe/src/main/scala/app/softnetwork/payment/spi/StripeAccountApi.scala +++ b/stripe/src/main/scala/app/softnetwork/payment/spi/StripeAccountApi.scala @@ -101,6 +101,14 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => /** @param maybeNaturalUser * - natural user to create + * @param acceptedTermsOfPSP + * - whether the user has accepted the terms of the PSP + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the payment account * @return * provider user id */ @@ -109,7 +117,8 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => maybeNaturalUser: Option[NaturalUser], acceptedTermsOfPSP: Boolean, ipAddress: Option[String], - userAgent: Option[String] + userAgent: Option[String], + tokenId: Option[String] ): Option[String] = { maybeNaturalUser match { case Some(naturalUser) => @@ -147,55 +156,62 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => .find(acc => acc.getMetadata.get("external_uuid") == naturalUser.externalUuid) }) match { case Some(account) => - mlog.info(s"account -> ${new Gson().toJson(account)}") + mlog.info(s"individual account to update -> ${new Gson().toJson(account)}") - // update account - val individual = - TokenCreateParams.Account.Individual - .builder() - .setFirstName(naturalUser.firstName) - .setLastName(naturalUser.lastName) - .setDob( - TokenCreateParams.Account.Individual.Dob - .builder() - .setDay(c.get(Calendar.DAY_OF_MONTH)) - .setMonth(c.get(Calendar.MONTH) + 1) - .setYear(c.get(Calendar.YEAR)) - .build() - ) - .setEmail(naturalUser.email) - naturalUser.phone match { - case Some(phone) => - individual.setPhone(phone) + // update individual account + val token = tokenId match { + case Some(t) => + Token.retrieve(t, StripeApi().requestOptions()) case _ => - } - naturalUser.address match { - case Some(address) => - individual.setAddress( - TokenCreateParams.Account.Individual.Address + // create account with token + val individual = + TokenCreateParams.Account.Individual .builder() - .setCity(address.city) - .setCountry(address.country) - .setLine1(address.addressLine) - .setPostalCode(address.postalCode) - .build() + .setFirstName(naturalUser.firstName) + .setLastName(naturalUser.lastName) + .setDob( + TokenCreateParams.Account.Individual.Dob + .builder() + .setDay(c.get(Calendar.DAY_OF_MONTH)) + .setMonth(c.get(Calendar.MONTH) + 1) + .setYear(c.get(Calendar.YEAR)) + .build() + ) + .setEmail(naturalUser.email) + naturalUser.phone match { + case Some(phone) => + individual.setPhone(phone) + case _ => + } + naturalUser.address match { + case Some(address) => + individual.setAddress( + TokenCreateParams.Account.Individual.Address + .builder() + .setCity(address.city) + .setCountry(address.country) + .setLine1(address.addressLine) + .setPostalCode(address.postalCode) + .build() + ) + case _ => + } + val params = TokenCreateParams.Account + .builder() + .setBusinessType(TokenCreateParams.Account.BusinessType.INDIVIDUAL) + .setIndividual(individual.build()) + .setTosShownAndAccepted(tos_shown_and_accepted) + mlog.info( + s"update individual account token params -> ${new Gson().toJson(params.build())}" ) - case _ => - } - val token = Token.create( - TokenCreateParams - .builder() - .setAccount( - TokenCreateParams.Account + Token.create( + TokenCreateParams .builder() - .setBusinessType(TokenCreateParams.Account.BusinessType.INDIVIDUAL) - .setIndividual(individual.build()) - .setTosShownAndAccepted(tos_shown_and_accepted) - .build + .setAccount(params.build()) + .build(), + StripeApi().requestOptions() ) - .build(), - StripeApi().requestOptions() - ) + } val params = AccountUpdateParams .builder() @@ -285,6 +301,9 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => params.setBusinessProfile(businessProfile.build()) case _ => } + mlog.info( + s"update individual account params -> ${new Gson().toJson(params.build())}" + ) account.update( params.build(), StripeApi().requestOptions() @@ -292,52 +311,60 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => case _ => // create account - val individual = - TokenCreateParams.Account.Individual - .builder() - .setFirstName(naturalUser.firstName) - .setLastName(naturalUser.lastName) - .setDob( - TokenCreateParams.Account.Individual.Dob - .builder() - .setDay(c.get(Calendar.DAY_OF_MONTH)) - .setMonth(c.get(Calendar.MONTH) + 1) - .setYear(c.get(Calendar.YEAR)) - .build() - ) - .setEmail(naturalUser.email) - naturalUser.phone match { - case Some(phone) => - individual.setPhone(phone) - case _ => - } - naturalUser.address match { - case Some(address) => - individual.setAddress( - TokenCreateParams.Account.Individual.Address - .builder() - .setCity(address.city) - .setCountry(address.country) - .setLine1(address.addressLine) - .setPostalCode(address.postalCode) - .build() - ) - case _ => - } - val token = Token.create( - TokenCreateParams - .builder() - .setAccount( - TokenCreateParams.Account + val token = { + tokenId match { + case Some(t) => + Token.retrieve(t, StripeApi().requestOptions()) + case _ => + val individual = + TokenCreateParams.Account.Individual + .builder() + .setFirstName(naturalUser.firstName) + .setLastName(naturalUser.lastName) + .setDob( + TokenCreateParams.Account.Individual.Dob + .builder() + .setDay(c.get(Calendar.DAY_OF_MONTH)) + .setMonth(c.get(Calendar.MONTH) + 1) + .setYear(c.get(Calendar.YEAR)) + .build() + ) + .setEmail(naturalUser.email) + naturalUser.phone match { + case Some(phone) => + individual.setPhone(phone) + case _ => + } + naturalUser.address match { + case Some(address) => + individual.setAddress( + TokenCreateParams.Account.Individual.Address + .builder() + .setCity(address.city) + .setCountry(address.country) + .setLine1(address.addressLine) + .setPostalCode(address.postalCode) + .build() + ) + case _ => + } + val params = TokenCreateParams.Account .builder() .setBusinessType(TokenCreateParams.Account.BusinessType.INDIVIDUAL) .setIndividual(individual.build()) .setTosShownAndAccepted(tos_shown_and_accepted) - .build() - ) - .build(), - StripeApi().requestOptions() - ) + mlog.info( + s"create individual account token params -> ${new Gson().toJson(params.build())}" + ) + Token.create( + TokenCreateParams + .builder() + .setAccount(params.build()) + .build(), + StripeApi().requestOptions() + ) + } + } val params = AccountCreateParams .builder() @@ -458,6 +485,10 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => case _ => } + mlog.info( + s"create individual account params -> ${new Gson().toJson(params.build())}" + ) + Account.create( params.build(), StripeApi().requestOptions() @@ -502,6 +533,14 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => /** @param maybeLegalUser * - legal user to create or update + * @param acceptedTermsOfPSP + * - whether the user has accepted the terms of the PSP + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the payment account * @return * legal user created or updated */ @@ -510,7 +549,8 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => maybeLegalUser: Option[LegalUser], acceptedTermsOfPSP: Boolean, ipAddress: Option[String], - userAgent: Option[String] + userAgent: Option[String], + tokenId: Option[String] ): Option[String] = { maybeLegalUser match { case Some(legalUser) => @@ -588,58 +628,65 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => ) } - // update account company - val company = - TokenCreateParams.Account.Company - .builder() - .setName(legalUser.legalName) - .setAddress( - TokenCreateParams.Account.Company.Address - .builder() - .setCity(legalUser.headQuartersAddress.city) - .setCountry(legalUser.headQuartersAddress.country) - .setLine1(legalUser.headQuartersAddress.addressLine) - .setPostalCode(legalUser.headQuartersAddress.postalCode) - .build() - ) - .setTaxId(legalUser.siren) - .setDirectorsProvided(true) - .setExecutivesProvided(true) - - if (soleTrader) { - company - .setOwnersProvided(true) - } + val token = { + tokenId match { + case Some(t) => + Token.retrieve(t, requestOptions) + case _ => + val company = + TokenCreateParams.Account.Company + .builder() + .setName(legalUser.legalName) + .setAddress( + TokenCreateParams.Account.Company.Address + .builder() + .setCity(legalUser.headQuartersAddress.city) + .setCountry(legalUser.headQuartersAddress.country) + .setLine1(legalUser.headQuartersAddress.addressLine) + .setPostalCode(legalUser.headQuartersAddress.postalCode) + .build() + ) + .setTaxId(legalUser.siren) + .setDirectorsProvided(true) + .setExecutivesProvided(true) - legalUser.vatNumber match { - case Some(vatNumber) => - company.setVatId(vatNumber) - case _ => - } + if (soleTrader) { + company + .setOwnersProvided(true) + } - legalUser.phone.orElse(legalUser.legalRepresentative.phone) match { - case Some(phone) => - company.setPhone(phone) - case _ => - } + legalUser.vatNumber match { + case Some(vatNumber) => + company.setVatId(vatNumber) + case _ => + } - mlog.info(s"company -> ${new Gson().toJson(company.build())}") + legalUser.phone.orElse(legalUser.legalRepresentative.phone) match { + case Some(phone) => + company.setPhone(phone) + case _ => + } - val token = - Token.create( - TokenCreateParams - .builder() - .setAccount( + val params = TokenCreateParams.Account .builder() .setBusinessType(TokenCreateParams.Account.BusinessType.COMPANY) .setCompany(company.build()) .setTosShownAndAccepted(tos_shown_and_accepted) - .build + + mlog.info( + s"update business account token params -> ${new Gson().toJson(params.build())}" ) - .build(), - requestOptions - ) + + Token.create( + TokenCreateParams + .builder() + .setAccount(params.build()) + .build(), + requestOptions + ) + } + } val params = AccountUpdateParams @@ -746,123 +793,74 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => case _ => } + mlog.info( + s"update business account params -> ${new Gson().toJson(params.build())}" + ) + account.update( params.build(), requestOptions ) - /*val person = - TokenCreateParams.Person - .builder() - .setFirstName(legalUser.legalRepresentative.firstName) - .setLastName(legalUser.legalRepresentative.lastName) - .setDob( - TokenCreateParams.Person.Dob - .builder() - .setDay(c.get(Calendar.DAY_OF_MONTH)) - .setMonth(c.get(Calendar.MONTH) + 1) - .setYear(c.get(Calendar.YEAR)) - .build() - ) - .setEmail(legalUser.legalRepresentative.email) - .setNationality(legalUser.legalRepresentative.nationality) - .setRelationship( - TokenCreateParams.Person.Relationship - .builder() - .setRepresentative(true) - .build() - ) - if(tos_shown_and_accepted) - person.setAdditionalTosAcceptances( - TokenCreateParams.Person.AdditionalTosAcceptances.builder() - .setAccount(TokenCreateParams.Person.AdditionalTosAcceptances.Account - .builder() - .setIp(ipAddress.get) - .setUserAgent(userAgent.get) - .setDate(persistence.now().getEpochSecond) - .build() - ) - .build() - ) - legalUser.legalRepresentative.phone match { - case Some(phone) => - person.setPhone(phone) - case _ => - } - - mlog.info(s"person -> ${new Gson().toJson(person.build())}") - - val token2 = - Token.create( - TokenCreateParams.builder.setPerson(person.build()).build(), - requestOptions - ) - - val params2 = - PersonCollectionCreateParams - .builder() - .setPersonToken(token2.getId) - - account - .persons() - .create( - params2.build(), - requestOptions - )*/ - account case _ => // create company account - val company = - TokenCreateParams.Account.Company - .builder() - .setName(legalUser.legalName) - .setAddress( - TokenCreateParams.Account.Company.Address - .builder() - .setCity(legalUser.headQuartersAddress.city) - .setCountry(legalUser.headQuartersAddress.country) - .setLine1(legalUser.headQuartersAddress.addressLine) - .setPostalCode(legalUser.headQuartersAddress.postalCode) - .build() - ) - .setTaxId(legalUser.siren) - .setDirectorsProvided(true) - .setExecutivesProvided(true) - - if (soleTrader) { - company - .setOwnersProvided(true) - } + val token = { + tokenId match { + case Some(t) => + Token.retrieve(t, StripeApi().requestOptions()) + case _ => + val company = + TokenCreateParams.Account.Company + .builder() + .setName(legalUser.legalName) + .setAddress( + TokenCreateParams.Account.Company.Address + .builder() + .setCity(legalUser.headQuartersAddress.city) + .setCountry(legalUser.headQuartersAddress.country) + .setLine1(legalUser.headQuartersAddress.addressLine) + .setPostalCode(legalUser.headQuartersAddress.postalCode) + .build() + ) + .setTaxId(legalUser.siren) + .setDirectorsProvided(true) + .setExecutivesProvided(true) - legalUser.vatNumber match { - case Some(vatNumber) => - company.setVatId(vatNumber) - case _ => - } + if (soleTrader) { + company + .setOwnersProvided(true) + } - legalUser.phone.orElse(legalUser.legalRepresentative.phone) match { - case Some(phone) => - company.setPhone(phone) - case _ => - } + legalUser.vatNumber match { + case Some(vatNumber) => + company.setVatId(vatNumber) + case _ => + } - mlog.info(s"company -> ${new Gson().toJson(company.build())}") + legalUser.phone.orElse(legalUser.legalRepresentative.phone) match { + case Some(phone) => + company.setPhone(phone) + case _ => + } - val accountParams = - TokenCreateParams.Account - .builder() - .setBusinessType(TokenCreateParams.Account.BusinessType.COMPANY) - .setCompany(company.build()) - .setTosShownAndAccepted(tos_shown_and_accepted) + val accountParams = + TokenCreateParams.Account + .builder() + .setBusinessType(TokenCreateParams.Account.BusinessType.COMPANY) + .setCompany(company.build()) + .setTosShownAndAccepted(tos_shown_and_accepted) - mlog.info(s"token -> ${new Gson().toJson(accountParams.build())}") + mlog.info(s"create business account token params -> ${new Gson() + .toJson(accountParams.build())}") - val token = Token.create( - TokenCreateParams.builder.setAccount(accountParams.build()).build(), - StripeApi().requestOptions() - ) + Token.create( + TokenCreateParams.builder.setAccount(accountParams.build()).build(), + StripeApi().requestOptions() + ) + } + } val params = AccountCreateParams @@ -998,7 +996,9 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => case _ => } - mlog.info(s"account -> ${new Gson().toJson(params.build())}") + mlog.info( + s"create business account params -> ${new Gson().toJson(params.build())}" + ) val account = Account.create( params.build(), @@ -1153,6 +1153,12 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => * - Provider user id * @param uboDeclarationId * - Provider declaration id + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the payment account * @return * Ultimate Beneficial Owner declaration */ @@ -1160,7 +1166,8 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => userId: String, uboDeclarationId: String, ipAddress: String, - userAgent: String + userAgent: String, + tokenId: Option[String] ): Option[UboDeclaration] = { Try { val account = Account.retrieve(userId, StripeApi().requestOptions()) @@ -1185,35 +1192,42 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => val ownersProvided = persons.map(_.getRelationship.getPercentOwnership.doubleValue()).sum == 100.0 - val company = - TokenCreateParams.Account.Company - .builder() - .setOwnershipDeclaration( - TokenCreateParams.Account.Company.OwnershipDeclaration - .builder() - .setDate(persistence.now().getEpochSecond) - .setIp(ipAddress) - .setUserAgent(userAgent) - .build() - ) - .setOwnershipDeclarationShownAndSigned(ownersProvided) - .setOwnersProvided(ownersProvided) - - mlog.info(s"company -> ${new Gson().toJson(company.build())}") + val token = { + tokenId match { + case Some(t) => + Token.retrieve(t, StripeApi().requestOptions()) + case _ => + val company = + TokenCreateParams.Account.Company + .builder() + .setOwnershipDeclaration( + TokenCreateParams.Account.Company.OwnershipDeclaration + .builder() + .setDate(persistence.now().getEpochSecond) + .setIp(ipAddress) + .setUserAgent(userAgent) + .build() + ) + .setOwnershipDeclarationShownAndSigned(ownersProvided) + .setOwnersProvided(ownersProvided) - val token = Token.create( - TokenCreateParams.builder - .setAccount( - TokenCreateParams.Account + val params = TokenCreateParams.Account .builder() .setBusinessType(TokenCreateParams.Account.BusinessType.COMPANY) .setCompany(company.build()) .setTosShownAndAccepted(true) - .build() - ) - .build(), - StripeApi().requestOptions() - ) + + mlog.info(s"validate declaration token params -> ${new Gson().toJson(params.build())}") + + // create a token for the account + Token.create( + TokenCreateParams.builder + .setAccount(params.build()) + .build(), + StripeApi().requestOptions() + ) + } + } account.update( AccountUpdateParams @@ -1655,33 +1669,48 @@ trait StripeAccountApi extends PaymentAccountApi { _: StripeContext => /** @param maybeBankAccount * - bank account to create + * @param bankTokenId + * - optional bank token id for the payment account * @return * bank account id */ - override def createOrUpdateBankAccount(maybeBankAccount: Option[BankAccount]): Option[String] = { + override def createOrUpdateBankAccount( + maybeBankAccount: Option[BankAccount], + bankTokenId: Option[String] + ): Option[String] = { maybeBankAccount match { case Some(bankAccount) => val requestOptions = StripeApi().requestOptions() Try(Account.retrieve(bankAccount.userId, requestOptions)) match { case Success(account) => Try { - val bank_account = - TokenCreateParams.BankAccount - .builder() - .setAccountNumber(bankAccount.iban) - .setRoutingNumber(bankAccount.bic) - .setCountry(bankAccount.countryCode.getOrElse(bankAccount.ownerAddress.country)) - .setCurrency(bankAccount.currency.getOrElse("EUR")) - .setAccountHolderName(bankAccount.ownerName) - .setAccountHolderType(account.getBusinessType match { - case "individual" => TokenCreateParams.BankAccount.AccountHolderType.INDIVIDUAL - case _ => TokenCreateParams.BankAccount.AccountHolderType.COMPANY - }) - .build() - Token.create( - TokenCreateParams.builder().setBankAccount(bank_account).build(), - requestOptions - ) + bankTokenId match { + case Some(tokenId) => + Token.retrieve(tokenId, requestOptions) + case _ => + // create a token for the bank account + val bank_account = + TokenCreateParams.BankAccount + .builder() + .setAccountNumber(bankAccount.iban) + .setRoutingNumber(bankAccount.bic) + .setCountry( + bankAccount.countryCode.getOrElse(bankAccount.ownerAddress.country) + ) + .setCurrency(bankAccount.currency.getOrElse("EUR")) + .setAccountHolderName(bankAccount.ownerName) + .setAccountHolderType(account.getBusinessType match { + case "individual" => + TokenCreateParams.BankAccount.AccountHolderType.INDIVIDUAL + case _ => TokenCreateParams.BankAccount.AccountHolderType.COMPANY + }) + .build() + mlog.info(s"token bank account params -> ${new Gson().toJson(bank_account)}") + Token.create( + TokenCreateParams.builder().setBankAccount(bank_account).build(), + requestOptions + ) + } } match { case Success(token) => val fingerprint = token.getBankAccount.getFingerprint diff --git a/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentRouteSpec.scala b/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentRouteSpec.scala index 4b9d8ff..de99040 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentRouteSpec.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentRouteSpec.scala @@ -40,6 +40,8 @@ trait PaymentRouteSpec[SD <: SessionData with SessionDataDecorator[SD]] BankAccountCommand( BankAccount(None, ownerName, ownerAddress, "", bic), naturalUser, + None, + None, None ) ) @@ -56,6 +58,8 @@ trait PaymentRouteSpec[SD <: SessionData with SessionDataDecorator[SD]] BankAccountCommand( BankAccount(None, ownerName, ownerAddress, iban, "WRONG"), naturalUser, + None, + None, None ) ) @@ -70,7 +74,9 @@ trait PaymentRouteSpec[SD <: SessionData with SessionDataDecorator[SD]] BankAccountCommand( BankAccount(None, ownerName, ownerAddress, iban, bic), naturalUser.withExternalUuid(externalUserId), - Some(true) + Some(true), + None, + None ) log.info(s"create bank account with natural user command: ${serialization.write(command)}") withHeaders( @@ -95,6 +101,8 @@ trait PaymentRouteSpec[SD <: SessionData with SessionDataDecorator[SD]] BankAccountCommand( BankAccount(Option(sellerBankAccountId), ownerName, ownerAddress, iban, bic), naturalUser.withLastName("anotherLastName").withExternalUuid(externalUserId), + None, + None, None ) ) @@ -120,6 +128,8 @@ trait PaymentRouteSpec[SD <: SessionData with SessionDataDecorator[SD]] bic ), legalUser.withSiret(""), + None, + None, None ) ) @@ -142,6 +152,8 @@ trait PaymentRouteSpec[SD <: SessionData with SessionDataDecorator[SD]] bic ), legalUser.withLegalName(""), + None, + None, None ) ) @@ -164,6 +176,8 @@ trait PaymentRouteSpec[SD <: SessionData with SessionDataDecorator[SD]] bic ), legalUser, + None, + None, None ) ) @@ -185,7 +199,9 @@ trait PaymentRouteSpec[SD <: SessionData with SessionDataDecorator[SD]] bic ), legalUser.withLegalRepresentative(naturalUser.withExternalUuid(externalUserId)), - Some(true) + Some(true), + None, + None ) log.info( s"update bank account with sole trader legal user command: ${serialization.write(command)}" @@ -221,7 +237,9 @@ trait PaymentRouteSpec[SD <: SessionData with SessionDataDecorator[SD]] legalUser .withLegalUserType(LegalUser.LegalUserType.BUSINESS) .withLegalRepresentative(naturalUser.withExternalUuid(externalUserId)), - Some(true) + Some(true), + None, + None ) log.info( s"update bank account with business legal user command: ${serialization.write(command)}" diff --git a/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentRouteTestKit.scala b/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentRouteTestKit.scala index a618f43..f5fd9f8 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentRouteTestKit.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentRouteTestKit.scala @@ -16,8 +16,8 @@ import com.stripe.net.Webhook import org.scalatest.Suite import java.time.Instant +import scala.sys.process.Process import scala.util.{Failure, Success, Try} -import sys.process._ trait StripePaymentRouteTestKit[SD <: SessionData with SessionDataDecorator[SD]] extends PaymentRouteTestKit[SD] diff --git a/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentTestKit.scala b/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentTestKit.scala index a88a1df..efc9649 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentTestKit.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentTestKit.scala @@ -1,10 +1,151 @@ package app.softnetwork.payment.scalatest import app.softnetwork.payment.config.{StripeApi, StripeSettings} +import app.softnetwork.payment.model.{BankAccount, PaymentAccount} +import com.google.gson.Gson +import com.stripe.model.Token +import com.stripe.param.TokenCreateParams import org.scalatest.Suite trait StripePaymentTestKit extends PaymentTestKit { _: Suite => override implicit lazy val providerConfig: StripeApi.Config = StripeSettings.StripeApiConfig + def createAccountToken(paymentAccount: PaymentAccount): Token = { + if (paymentAccount.user.isNaturalUser) { + paymentAccount.user.naturalUser match { + case Some(naturalUser) => + val birthday = naturalUser.birthday.split("/") + val individual = + TokenCreateParams.Account.Individual + .builder() + .setFirstName(naturalUser.firstName) + .setLastName(naturalUser.lastName) + .setDob( + TokenCreateParams.Account.Individual.Dob + .builder() + .setDay(birthday(0).toInt) + .setMonth(birthday(1).toInt) + .setYear(birthday(2).toInt) + .build() + ) + .setEmail(naturalUser.email) + naturalUser.phone match { + case Some(phone) => + individual.setPhone(phone) + case _ => + } + naturalUser.address match { + case Some(address) => + individual.setAddress( + TokenCreateParams.Account.Individual.Address + .builder() + .setCity(address.city) + .setCountry(address.country) + .setLine1(address.addressLine) + .setPostalCode(address.postalCode) + .build() + ) + case _ => + } + val params = TokenCreateParams.Account + .builder() + .setBusinessType(TokenCreateParams.Account.BusinessType.INDIVIDUAL) + .setIndividual(individual.build()) + .setTosShownAndAccepted(true) + log.info(s"individual account token params -> ${new Gson().toJson(params.build())}") + Token.create( + TokenCreateParams + .builder() + .setAccount(params.build()) + .build(), + StripeApi().requestOptions() + ) + case _ => fail("Natural user is not defined for the payment account") + } + } else { + paymentAccount.user.legalUser match { + case Some(legalUser) => + val soleTrader = legalUser.legalUserType.isSoletrader + val company = + TokenCreateParams.Account.Company + .builder() + .setName(legalUser.legalName) + .setAddress( + TokenCreateParams.Account.Company.Address + .builder() + .setCity(legalUser.headQuartersAddress.city) + .setCountry(legalUser.headQuartersAddress.country) + .setLine1(legalUser.headQuartersAddress.addressLine) + .setPostalCode(legalUser.headQuartersAddress.postalCode) + .build() + ) + .setTaxId(legalUser.siren) + .setDirectorsProvided(true) + .setExecutivesProvided(true) + + if (soleTrader) { + company + .setOwnersProvided(true) + } + + legalUser.vatNumber match { + case Some(vatNumber) => + company.setVatId(vatNumber) + case _ => + } + + legalUser.phone.orElse(legalUser.legalRepresentative.phone) match { + case Some(phone) => + company.setPhone(phone) + case _ => + } + + val params = + TokenCreateParams.Account + .builder() + .setBusinessType(TokenCreateParams.Account.BusinessType.COMPANY) + .setCompany(company.build()) + .setTosShownAndAccepted(true) + log.info(s"business account token params -> ${new Gson().toJson(params.build())}") + Token.create( + TokenCreateParams + .builder() + .setAccount(params.build()) + .build(), + StripeApi().requestOptions() + ) + case _ => fail("Legal user is not defined for the payment account") + } + } + } + + def createBankToken(bankAccount: BankAccount, individual: Boolean): Token = { + val params = TokenCreateParams.BankAccount + .builder() + .setAccountNumber(bankAccount.iban) + .setRoutingNumber(bankAccount.bic) + .setCountry( + bankAccount.countryCode.getOrElse(bankAccount.ownerAddress.country) + ) + .setCurrency(bankAccount.currency.getOrElse("EUR")) + .setAccountHolderName(bankAccount.ownerName) + .setAccountHolderType(if (individual) { + TokenCreateParams.BankAccount.AccountHolderType.INDIVIDUAL + } else { + TokenCreateParams.BankAccount.AccountHolderType.COMPANY + }) + log.info(s"bank account token params -> ${new Gson().toJson(params.build())}") + Token.create( + TokenCreateParams + .builder() + .setBankAccount(params.build()) + .build(), + StripeApi().requestOptions() + ) + } + + def createValidationOfDeclarationToken(): Token = { + ??? + } } diff --git a/testkit/src/main/scala/app/softnetwork/payment/spi/MockMangoPayProvider.scala b/testkit/src/main/scala/app/softnetwork/payment/spi/MockMangoPayProvider.scala index bc14684..92987f9 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/spi/MockMangoPayProvider.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/spi/MockMangoPayProvider.scala @@ -54,6 +54,14 @@ trait MockMangoPayProvider extends MangoPayProvider with Entity { /** @param maybeNaturalUser * - natural user to create + * @param acceptedTermsOfPSP + * - whether the user has accepted the terms of the PSP + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the payment account * @return * provider user id */ @@ -62,7 +70,8 @@ trait MockMangoPayProvider extends MangoPayProvider with Entity { maybeNaturalUser: Option[NaturalUser], acceptedTermsOfPSP: Boolean, ipAddress: Option[String], - userAgent: Option[String] + userAgent: Option[String], + tokenId: Option[String] ): Option[String] = maybeNaturalUser match { case Some(naturalUser) => @@ -108,6 +117,14 @@ trait MockMangoPayProvider extends MangoPayProvider with Entity { /** @param maybeLegalUser * - legal user to create + * @param acceptedTermsOfPSP + * - whether the user has accepted the terms of the PSP + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the payment account * @return * provider user id */ @@ -116,7 +133,8 @@ trait MockMangoPayProvider extends MangoPayProvider with Entity { maybeLegalUser: Option[LegalUser], acceptedTermsOfPSP: Boolean, ipAddress: Option[String], - userAgent: Option[String] + userAgent: Option[String], + tokenId: Option[String] ): Option[String] = { maybeLegalUser match { case Some(legalUser) => @@ -250,10 +268,15 @@ trait MockMangoPayProvider extends MangoPayProvider with Entity { /** @param maybeBankAccount * - bank account to create + * @param tokenId + * - optional token id for the payment account * @return * bank account id */ - override def createOrUpdateBankAccount(maybeBankAccount: Option[BankAccount]): Option[String] = + override def createOrUpdateBankAccount( + maybeBankAccount: Option[BankAccount], + tokenId: Option[String] + ): Option[String] = maybeBankAccount match { case Some(mangoPayBankAccount) => import mangoPayBankAccount._ @@ -1417,6 +1440,12 @@ trait MockMangoPayProvider extends MangoPayProvider with Entity { * - Provider user id * @param uboDeclarationId * - Provider declaration id + * @param ipAddress + * - ip address of the user + * @param userAgent + * - user agent of the user + * @param tokenId + * - optional token id for the payment account * @return * Ultimate Beneficial Owner declaration */ @@ -1424,7 +1453,8 @@ trait MockMangoPayProvider extends MangoPayProvider with Entity { userId: String, uboDeclarationId: String, ipAddress: String, - userAgent: String + userAgent: String, + tokenId: Option[String] ): Option[UboDeclaration] = { UboDeclarations.get(uboDeclarationId) match { case Some(uboDeclaration) => diff --git a/testkit/src/test/scala/app/softnetwork/payment/handlers/PaymentHandlerSpec.scala b/testkit/src/test/scala/app/softnetwork/payment/handlers/PaymentHandlerSpec.scala index 7b3cec5..1221e7a 100644 --- a/testkit/src/test/scala/app/softnetwork/payment/handlers/PaymentHandlerSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/payment/handlers/PaymentHandlerSpec.scala @@ -161,6 +161,10 @@ class PaymentHandlerSpec } } + "pre authorize without card pre registration" in { + // TODO + } + "not create bank account with wrong iban" in { !?( CreateOrUpdateBankAccount( @@ -799,7 +803,8 @@ class PaymentHandlerSpec ValidateUboDeclaration( computeExternalUuidWithProfile(sellerUuid, Some("seller")), "127.0.0.1", - Some("UserAgent") + Some("UserAgent"), + None ) ) await { case UboDeclarationAskedForValidation => diff --git a/testkit/src/test/scala/app/softnetwork/payment/service/StripePaymentServiceSpec.scala b/testkit/src/test/scala/app/softnetwork/payment/service/StripePaymentServiceSpec.scala index 970379a..a946f1e 100644 --- a/testkit/src/test/scala/app/softnetwork/payment/service/StripePaymentServiceSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/payment/service/StripePaymentServiceSpec.scala @@ -5,7 +5,7 @@ import akka.http.scaladsl.model.headers.{`User-Agent`, `X-Forwarded-For`} import app.softnetwork.api.server.ApiRoutes import app.softnetwork.api.server.config.ServerSettings.RootPath import app.softnetwork.payment.config.PaymentSettings.PaymentConfig._ -import app.softnetwork.payment.config.{PaymentSettings, StripeApi, StripeSettings} +import app.softnetwork.payment.config.{PaymentSettings, StripeApi} import app.softnetwork.payment.data.{ bic, birthday, @@ -47,6 +47,7 @@ import app.softnetwork.payment.model.{ computeExternalUuidWithProfile, BankAccount, LegalUser, + PaymentAccount, PreRegistration, RecurringPayment, RecurringPaymentView, @@ -80,8 +81,6 @@ trait StripePaymentServiceSpec[SD <: SessionData with SessionDataDecorator[SD]] override lazy val log: Logger = LoggerFactory getLogger getClass.getName - override implicit lazy val providerConfig: StripeApi.Config = StripeSettings.StripeApiConfig - import app.softnetwork.serialization._ var customer: String = _ @@ -96,15 +95,21 @@ trait StripePaymentServiceSpec[SD <: SessionData with SessionDataDecorator[SD]] "individual account" should { "be created or updated" in { - externalUserId = "individual" + externalUserId = "individual-production" + val user = naturalUser.withExternalUuid(externalUserId) + val token = createAccountToken(PaymentAccount.defaultInstance.withNaturalUser(user)) + val bankAccount = BankAccount(None, ownerName, ownerAddress, iban, bic) + val bankToken = createBankToken(bankAccount, individual = true) createNewSession(sellerSession(externalUserId)) withHeaders( Post( s"/$RootPath/${PaymentSettings.PaymentConfig.path}/$bankRoute", BankAccountCommand( - BankAccount(None, ownerName, ownerAddress, iban, bic), - naturalUser.withExternalUuid(externalUserId), - Some(true) + bankAccount, + user, + Some(true), + Some(token.getId), + Some(bankToken.getId) ) ).withHeaders( `X-Forwarded-For`(RemoteAddress(InetAddress.getLocalHost)), @@ -132,9 +137,6 @@ trait StripePaymentServiceSpec[SD <: SessionData with SessionDataDecorator[SD]] } } - val probe = createTestProbe[PaymentResult]() - subscribeProbe(probe) - "register recurring direct debit payment" in { withHeaders( Post( @@ -178,6 +180,8 @@ trait StripePaymentServiceSpec[SD <: SessionData with SessionDataDecorator[SD]] } "execute direct debit automatically for next recurring payment" in { + val probe = createTestProbe[PaymentResult]() + subscribeProbe(probe) withHeaders( Delete(s"/$RootPath/${PaymentSettings.PaymentConfig.path}/$mandateRoute") ) ~> routes ~> check { @@ -217,19 +221,25 @@ trait StripePaymentServiceSpec[SD <: SessionData with SessionDataDecorator[SD]] "sole trader account" should { "be created or updated" in { - externalUserId = "soleTrader" + externalUserId = "soleTrader-production" + val user = legalUser.withLegalRepresentative(naturalUser.withExternalUuid(externalUserId)) + val token = createAccountToken(PaymentAccount.defaultInstance.withLegalUser(user)) + val bankAccount = BankAccount( + Option(sellerBankAccountId), + ownerName, + ownerAddress, + iban, + bic + ) + val bankToken = createBankToken(bankAccount, individual = false) createNewSession(sellerSession(externalUserId)) val command = BankAccountCommand( - BankAccount( - Option(sellerBankAccountId), - ownerName, - ownerAddress, - iban, - bic - ), - legalUser.withLegalRepresentative(naturalUser.withExternalUuid(externalUserId)), - Some(true) + bankAccount, + user, + Some(true), + Some(token.getId), + Some(bankToken.getId) ) log.info(serialization.write(command)) withHeaders( @@ -250,21 +260,28 @@ trait StripePaymentServiceSpec[SD <: SessionData with SessionDataDecorator[SD]] "business account" should { "be created or updated" in { - externalUserId = "business" + externalUserId = "business-production" + val user = legalUser + .withLegalUserType(LegalUser.LegalUserType.BUSINESS) + .withLegalRepresentative(naturalUser.withExternalUuid(externalUserId)) + val token = createAccountToken(PaymentAccount.defaultInstance.withLegalUser(user)) + val bankAccount = + BankAccount( + Option(sellerBankAccountId), + ownerName, + ownerAddress, + iban, + bic + ) + val bankToken = createBankToken(bankAccount, individual = false) createNewSession(sellerSession(externalUserId)) val bank = BankAccountCommand( - BankAccount( - Option(sellerBankAccountId), - ownerName, - ownerAddress, - iban, - bic - ), - legalUser - .withLegalUserType(LegalUser.LegalUserType.BUSINESS) - .withLegalRepresentative(naturalUser.withExternalUuid(externalUserId)), - Some(true) + bankAccount, + user, + Some(true), + Some(token.getId), + Some(bankToken.getId) ) log.info(serialization.write(bank)) withHeaders( From 283f0c8eac71f68b94238d074eb05a40ebaa9a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Thu, 17 Jul 2025 09:51:28 +0200 Subject: [PATCH 2/5] update github workflows --- .github/workflows/build.yml | 6 ++++-- .github/workflows/release.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 464a1ec..ef813fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,8 +23,8 @@ permissions: jobs: test: - runs-on: self-hosted -# runs-on: ubuntu-latest +# runs-on: self-hosted + runs-on: ubuntu-latest env: # define Java options for both official sbt and sbt-extras JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 @@ -38,6 +38,8 @@ jobs: java-version: '8' distribution: 'temurin' # cache: 'sbt' + - name: Setup sbt launcher + uses: sbt/setup-sbt@v1 # - name: Run tests & Coverage Report # run: sbt coverage test coverageReport # - name: Upload coverage to Codecov diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 355bc65..a6b7204 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,8 +20,8 @@ permissions: jobs: release: - runs-on: self-hosted -# runs-on: ubuntu-latest +# runs-on: self-hosted + runs-on: ubuntu-latest env: # define Java options for both official sbt and sbt-extras JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 @@ -35,6 +35,8 @@ jobs: java-version: '8' distribution: 'temurin' cache: 'sbt' + - name: Setup sbt launcher + uses: sbt/setup-sbt@v1 - name: Run tests & Coverage Report run: STRIPE_CLIENT_ID=${{secrets.STRIPE_CLIENT_ID}} STRIPE_API_KEY=${{secrets.STRIPE_API_KEY}} sbt coverage test coverageReport - name: Upload coverage to Codecov From c7d19c14d5c98c215a71b33b912318d416474b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Thu, 17 Jul 2025 09:59:48 +0200 Subject: [PATCH 3/5] add sbt launcher for lint job --- .github/workflows/build.yml | 2 ++ .github/workflows/release.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef813fd..013dd6c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,5 +69,7 @@ jobs: java-version: '8' distribution: 'temurin' # cache: 'sbt' + - name: Setup sbt launcher + uses: sbt/setup-sbt@v1 - name: Formatting run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a6b7204..845717f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,5 +60,7 @@ jobs: java-version: '8' distribution: 'temurin' # cache: 'sbt' + - name: Setup sbt launcher + uses: sbt/setup-sbt@v1 - name: Formatting run: sbt scalafmtSbtCheck scalafmtCheck test:scalafmtCheck \ No newline at end of file From 9fbb6ded5429748e0580a3b1de68c92569ee8491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Thu, 17 Jul 2025 10:36:49 +0200 Subject: [PATCH 4/5] to fix lint issue --- .../payment/persistence/typed/PayInCommandHandler.scala | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/typed/PayInCommandHandler.scala b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PayInCommandHandler.scala index ded26c1..8aa8283 100644 --- a/core/src/main/scala/app/softnetwork/payment/persistence/typed/PayInCommandHandler.scala +++ b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PayInCommandHandler.scala @@ -597,9 +597,8 @@ trait PayInCommandHandler List( TransactionUpdatedEvent.defaultInstance .withDocument( - transaction.copy( - clientId = paymentAccount.clientId, - debitedUserId = paymentAccount.userId) + transaction + .copy(clientId = paymentAccount.clientId, debitedUserId = paymentAccount.userId) ) .withLastUpdated(lastUpdated) ) From 33b454df8015df0f0e9b1659a46ad51a81c7aab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Thu, 17 Jul 2025 10:40:11 +0200 Subject: [PATCH 5/5] to fix codacy issues --- .../softnetwork/payment/service/UboDeclarationEndpoints.scala | 3 ++- .../softnetwork/payment/scalatest/StripePaymentTestKit.scala | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala index 9a7e3ce..4feae77 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala @@ -77,9 +77,10 @@ trait UboDeclarationEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { ) ) .serverLogic(principal => { case (ipAddress, userAgent, tokenId) => + val session: SD = principal._2 run( ValidateUboDeclaration( - externalUuidWithProfile(principal._2), + externalUuidWithProfile(session), ipAddress, userAgent, tokenId diff --git a/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentTestKit.scala b/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentTestKit.scala index efc9649..607cf20 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentTestKit.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentTestKit.scala @@ -145,7 +145,4 @@ trait StripePaymentTestKit extends PaymentTestKit { _: Suite => ) } - def createValidationOfDeclarationToken(): Token = { - ??? - } }