diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca07cba..a614974 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,17 +38,17 @@ jobs: java-version: '8' distribution: 'temurin' # cache: 'sbt' - - name: Run tests & Coverage Report - run: sbt coverage test coverageReport - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - files: common/target/scala-2.12/coverage-report/cobertura.xml,core/target/scala-2.12/coverage-report/cobertura.xml,teskit/target/scala-2.12/coverage-report/cobertura.xml - flags: unittests - fail_ci_if_error: true - verbose: true - # - name: Run tests - # run: sbt test +# - name: Run tests & Coverage Report +# run: sbt coverage test coverageReport +# - name: Upload coverage to Codecov +# uses: codecov/codecov-action@v3 +# with: +# files: common/target/scala-2.12/coverage-report/cobertura.xml,core/target/scala-2.12/coverage-report/cobertura.xml,teskit/target/scala-2.12/coverage-report/cobertura.xml +# flags: unittests +# fail_ci_if_error: true +# verbose: true + - name: Run tests + run: sbt clean test # Optional: This step uploads information to the GitHub dependency graph and unblocking Dependabot alerts for the repository # - name: Upload dependency graph # uses: scalacenter/sbt-dependency-submission@ab086b50c947c9774b70f39fc7f6e20ca2706c91 diff --git a/build.sbt b/build.sbt index 9c289f4..9ff45f7 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ ThisBuild / organization := "app.softnetwork" name := "payment" -ThisBuild / version := "0.5.0" +ThisBuild / version := "0.6.0" ThisBuild / scalaVersion := "2.12.18" @@ -29,15 +29,23 @@ ThisBuild / libraryDependencies ++= Seq( Test / parallelExecution := false +lazy val client = project.in(file("client")) + .configs(IntegrationTest) + .settings(Defaults.itSettings, app.softnetwork.Info.infoSettings) + .enablePlugins(BuildInfoPlugin, AkkaGrpcPlugin, JavaAppPackaging, UniversalDeployPlugin) + lazy val common = project.in(file("common")) .configs(IntegrationTest) .settings(Defaults.itSettings) .enablePlugins(AkkaGrpcPlugin) + .dependsOn( + client % "compile->compile;test->test;it->it" + ) lazy val core = project.in(file("core")) .configs(IntegrationTest) .settings(Defaults.itSettings, app.softnetwork.Info.infoSettings) - .enablePlugins(BuildInfoPlugin) + .enablePlugins(BuildInfoPlugin, AkkaGrpcPlugin) .dependsOn( common % "compile->compile;test->test;it->it" ) @@ -49,6 +57,13 @@ lazy val mangopay = project.in(file("mangopay")) core % "compile->compile;test->test;it->it" ) +lazy val stripe = project.in(file("stripe")) + .configs(IntegrationTest) + .settings(Defaults.itSettings) + .dependsOn( + core % "compile->compile;test->test;it->it" + ) + lazy val api = project.in(file("mangopay/api")) .configs(IntegrationTest) .settings(Defaults.itSettings) @@ -65,6 +80,6 @@ lazy val testkit = project.in(file("testkit")) ) lazy val root = project.in(file(".")) - .aggregate(common, core, mangopay, testkit, api) + .aggregate(client, common, core, mangopay, stripe, testkit, api) .configs(IntegrationTest) .settings(Defaults.itSettings) diff --git a/client/build.sbt b/client/build.sbt new file mode 100644 index 0000000..05b3533 --- /dev/null +++ b/client/build.sbt @@ -0,0 +1,32 @@ +import com.typesafe.sbt.packager.SettingsHelper.makeDeploymentSettings + +organization := "app.softnetwork.payment" + +name := "softpay" + +maintainer := "stephane.manciot@gmail.com" + +akkaGrpcGeneratedSources := Seq(AkkaGrpc.Client) + +val jacksonExclusions = Seq( + ExclusionRule(organization = "com.fasterxml.jackson.core"), + ExclusionRule(organization = "com.fasterxml.jackson.databind"), + ExclusionRule(organization = "com.fasterxml.jackson.jaxrs"), + ExclusionRule(organization = "com.fasterxml.jackson.module"), + ExclusionRule(organization = "com.fasterxml.jackson.dataformat", "jackson-dataformat-yaml") +) + +libraryDependencies ++= Seq( + "app.softnetwork.account" %% "account-common" % Versions.account, + "app.softnetwork.account" %% "account-common" % Versions.account % "protobuf", + "app.softnetwork.api" %% "generic-server-api" % Versions.genericPersistence, + "app.softnetwork.protobuf" %% "scalapb-extensions" % "0.1.7", + "commons-validator" % "commons-validator" % "1.6", + "com.github.scopt" %% "scopt" % Versions.scopt, + "org.scalatra.scalate" %% "scalate-core" % Versions.scalate exclude ("org.scala-lang.modules", "scala-xml_2.12") exclude ("org.scala-lang.modules", "scala-parser-combinators_2.12"), + "com.hubspot.jinjava" % "jinjava" % Versions.jinja excludeAll (jacksonExclusions *) exclude ("com.google.guava", "guava") exclude ("org.apache.commons", "commons-lang3") +) + +Compile / mainClass := Some("app.softnetwork.payment.cli.Main") + +makeDeploymentSettings(Universal, packageBin in Universal, "zip") \ No newline at end of file diff --git a/client/src/main/protobuf/api/client.proto b/client/src/main/protobuf/api/client.proto new file mode 100644 index 0000000..d295c9f --- /dev/null +++ b/client/src/main/protobuf/api/client.proto @@ -0,0 +1,80 @@ +syntax = "proto3"; + +import "scalapb/scalapb.proto"; +import "google/protobuf/wrappers.proto"; + +package app.softnetwork.payment.api; + +option (scalapb.options) = { + import: "app.softnetwork.protobuf.ScalaPBTypeMappers._" + preserve_unknown_fields: false +}; + +service ClientServiceApi { + rpc SignUpClient (SignUpClientRequest) returns (SignUpClientResponse) {} + rpc ActivateClient (ActivateClientRequest) returns (ActivateClientResponse) {} + rpc GenerateClientTokens (GenerateClientTokensRequest) returns (ClientTokensResponse) {} + rpc RefreshClientTokens (RefreshClientTokensRequest) returns (ClientTokensResponse) {} +} + +enum ProviderType { + MOCK = 0; + MANGOPAY = 1; +// STRIPE = 2; +} + +message SignUpClientRequest { + string principal = 1; + string credentials = 2; + string provider_id = 3; + string provider_api_key = 4; + ProviderType provider_type = 5; +} + +message ClientCreated { + string client_id = 1; + string client_secret = 2; +} + +message SignUpClientResponse { + oneof signup { + ClientCreated client = 1; + string error = 2; + } +} + +message ActivateClientRequest { + string token = 1; +} + +message ActivateClientResponse { + oneof activation { + bool activated = 1; + string error = 2; + } +} + +message Tokens { + string access_token = 1; + string token_type = 2; + int64 expires_in = 3; + string refresh_token = 4; + google.protobuf.Int64Value refresh_token_expires_in = 5; +} + +message ClientTokensResponse { + oneof clientTokens { + Tokens tokens = 1; + string error = 2; + } +} + +message GenerateClientTokensRequest { + string client_id = 1; + string client_secret = 2; + google.protobuf.StringValue scope = 3; +} + +message RefreshClientTokensRequest { + string refresh_token = 1; +} diff --git a/common/src/main/protobuf/api/payment.proto b/client/src/main/protobuf/api/payment.proto similarity index 93% rename from common/src/main/protobuf/api/payment.proto rename to client/src/main/protobuf/api/payment.proto index c065d2f..7342fee 100644 --- a/common/src/main/protobuf/api/payment.proto +++ b/client/src/main/protobuf/api/payment.proto @@ -40,6 +40,7 @@ message PayInWithCardPreAuthorizedRequest { string preAuthorizationId = 1; string creditedAccount = 2; google.protobuf.Int32Value debitedAmount = 3; + string clientId = 4; } enum TransactionStatus{ @@ -59,6 +60,7 @@ message TransactionResponse { message CancelPreAuthorizationRequest { string orderUuid = 1; string cardPreAuthorizedTransactionId = 2; + string clientId = 3; } message CancelPreAuthorizationResponse { @@ -72,6 +74,7 @@ message RefundRequest { string currency = 4; string reasonMessage = 5; bool initializedByClient = 6; + string clientId = 7; } message PayOutRequest { @@ -81,6 +84,7 @@ message PayOutRequest { int32 feesAmount = 4; string currency = 5; google.protobuf.StringValue externalReference = 6; + string clientId = 7; } message TransferRequest { @@ -92,6 +96,7 @@ message TransferRequest { string currency = 6; bool payOutRequired = 7; google.protobuf.StringValue externalReference = 8; + string clientId = 9; } message TransferResponse { @@ -108,10 +113,12 @@ message DirectDebitRequest { string currency = 4; string statementDescriptor = 5; google.protobuf.StringValue externalReference = 6; + string clientId = 7; } message LoadDirectDebitTransactionRequest { string directDebitTransactionId = 1; + string clientId = 2; } message RegisterRecurringPaymentRequest { @@ -142,6 +149,9 @@ message RegisterRecurringPaymentRequest { google.protobuf.BoolValue fixedNextAmount = 9; google.protobuf.Int32Value nextDebitedAmount = 10; google.protobuf.Int32Value nextFeesAmount = 11; + google.protobuf.StringValue statementDescriptor = 12; + google.protobuf.StringValue externalReference = 13; + string clientId = 14; } message RegisterRecurringPaymentResponse { @@ -150,6 +160,7 @@ message RegisterRecurringPaymentResponse { message CancelMandateRequest { string externalUuid = 1; + string clientId = 2; } message CancelMandateResponse { @@ -158,6 +169,7 @@ message CancelMandateResponse { message LoadBankAccountOwnerRequest { string externalUuid = 1; + string clientId = 2; } message LoadBankAccountOwnerResponse { @@ -173,6 +185,7 @@ enum LegalUserType { message LoadLegalUserRequest { string externalUuid = 1; + string clientId = 2; } message LoadLegalUserResponse { diff --git a/common/src/main/protobuf/model/payment/address.proto b/client/src/main/protobuf/model/payment/address.proto similarity index 92% rename from common/src/main/protobuf/model/payment/address.proto rename to client/src/main/protobuf/model/payment/address.proto index a251150..20023a2 100644 --- a/common/src/main/protobuf/model/payment/address.proto +++ b/client/src/main/protobuf/model/payment/address.proto @@ -11,7 +11,6 @@ option (scalapb.options) = { import: "app.softnetwork.persistence.model._" import: "app.softnetwork.serialization._" import: "app.softnetwork.payment.model._" - import: "app.softnetwork.payment.serialization._" preserve_unknown_fields: false }; @@ -22,4 +21,5 @@ message Address { required string city = 2; required string postalCode = 3; required string country = 4 [default = "FR"]; + optional string state = 5; } diff --git a/common/src/main/protobuf/model/payment/card.proto b/client/src/main/protobuf/model/payment/card.proto similarity index 94% rename from common/src/main/protobuf/model/payment/card.proto rename to client/src/main/protobuf/model/payment/card.proto index 23861af..383ce37 100644 --- a/common/src/main/protobuf/model/payment/card.proto +++ b/client/src/main/protobuf/model/payment/card.proto @@ -11,7 +11,6 @@ option (scalapb.options) = { import: "app.softnetwork.persistence.model._" import: "app.softnetwork.serialization._" import: "app.softnetwork.payment.model._" - import: "app.softnetwork.payment.serialization._" preserve_unknown_fields: false }; diff --git a/common/src/main/protobuf/model/payment/document.proto b/client/src/main/protobuf/model/payment/document.proto similarity index 98% rename from common/src/main/protobuf/model/payment/document.proto rename to client/src/main/protobuf/model/payment/document.proto index ef70315..a640b2d 100644 --- a/common/src/main/protobuf/model/payment/document.proto +++ b/client/src/main/protobuf/model/payment/document.proto @@ -14,7 +14,6 @@ option (scalapb.options) = { import: "app.softnetwork.protobuf.ScalaPBTypeMappers._" import: "app.softnetwork.serialization._" import: "app.softnetwork.payment.model._" - import: "app.softnetwork.payment.serialization._" preserve_unknown_fields: false }; diff --git a/common/src/main/protobuf/model/payment/paymentUser.proto b/client/src/main/protobuf/model/payment/paymentUser.proto similarity index 94% rename from common/src/main/protobuf/model/payment/paymentUser.proto rename to client/src/main/protobuf/model/payment/paymentUser.proto index 6e61898..7251ebe 100644 --- a/common/src/main/protobuf/model/payment/paymentUser.proto +++ b/client/src/main/protobuf/model/payment/paymentUser.proto @@ -17,7 +17,6 @@ option (scalapb.options) = { import: "app.softnetwork.protobuf.ScalaPBTypeMappers._" import: "app.softnetwork.serialization._" import: "app.softnetwork.payment.model._" - import: "app.softnetwork.payment.serialization._" preserve_unknown_fields: false }; @@ -57,14 +56,14 @@ message BankAccount { optional MandateScheme mandateScheme = 14 [default = MANDATE_SEPA]; } -message PaymentUser { - enum PaymentUserType { +message NaturalUser { + enum NaturalUserType { PAYER = 0; COLLECTOR = 1; } option (scalapb.message).extends = "ProtobufDomainObject"; - option (scalapb.message).extends = "PaymentUserDecorator"; - option (scalapb.message).companion_extends = "PaymentUserCompanion"; + option (scalapb.message).extends = "NaturalUserDecorator"; + option (scalapb.message).companion_extends = "NaturalUserCompanion"; required string firstName = 1; required string lastName = 2; required string email = 3; @@ -75,7 +74,7 @@ message PaymentUser { optional string walletId = 8; required string externalUuid = 9; optional string profile = 10; - optional PaymentUserType paymentUserType = 11; + optional NaturalUserType naturalUserType = 11; // optional string secondaryWalletId = 12; } @@ -90,7 +89,7 @@ message LegalUser { required LegalUserType legalUserType = 1; required string legalName = 2; required string siret = 3; - required PaymentUser legalRepresentative = 4; + required NaturalUser legalRepresentative = 4; required Address legalRepresentativeAddress = 5; required Address headQuartersAddress = 6; optional UboDeclaration uboDeclaration = 7; @@ -114,7 +113,7 @@ message PaymentAccount { required google.protobuf.Timestamp createdDate = 2 [(scalapb.field).type = "java.time.Instant"]; required google.protobuf.Timestamp lastUpdated = 3 [(scalapb.field).type = "java.time.Instant"]; oneof user { - PaymentUser naturalUser = 4; + NaturalUser naturalUser = 4; LegalUser legalUser = 5; } repeated Card cards = 6; @@ -123,6 +122,7 @@ message PaymentAccount { required PaymentAccountStatus paymentAccountStatus = 9 [default = DOCUMENTS_KO]; repeated Transaction transactions = 10; repeated RecurringPayment recurryingPayments = 11; + optional string clientId = 12; } message MandateResult{ @@ -191,4 +191,6 @@ message RecurringPayment { optional int32 cumulatedFeesAmount = 20; optional bool migration = 21; optional string cardId = 22; + optional string statementDescriptor = 23; + optional string externalReference = 24; } \ No newline at end of file diff --git a/common/src/main/protobuf/model/payment/transaction.proto b/client/src/main/protobuf/model/payment/transaction.proto similarity index 99% rename from common/src/main/protobuf/model/payment/transaction.proto rename to client/src/main/protobuf/model/payment/transaction.proto index 463897c..c18df20 100644 --- a/common/src/main/protobuf/model/payment/transaction.proto +++ b/client/src/main/protobuf/model/payment/transaction.proto @@ -84,6 +84,7 @@ message Transaction { optional string returnUrl = 30; optional string payPalBuyerAccountEmail = 31; optional string idempotencyKey = 32; + optional string clientId = 33; } message BrowserInfo { diff --git a/client/src/main/resources/reference.conf b/client/src/main/resources/reference.conf new file mode 100644 index 0000000..015b7e2 --- /dev/null +++ b/client/src/main/resources/reference.conf @@ -0,0 +1,34 @@ +akka.http.session { + encrypt-data = true + + jws { + alg = "HS256" + } + + jwt { + iss = "soft-payment" + include-iat = true + } +} + +payment{ + client-id = "" + client-id = ${?PAYMENT_CLIENT_ID} + + api-key = "" + api-key = ${?PAYMENT_API_KEY} +} + +akka.http.server.preview.enable-http2 = on + +akka.grpc.client.PaymentService { + host = localhost + port = 9000 + use-tls = false +} + +akka.grpc.client.ClientService { + host = localhost + port = 9000 + use-tls = false +} diff --git a/client/src/main/scala/app/softnetwork/payment/api/Client.scala b/client/src/main/scala/app/softnetwork/payment/api/Client.scala new file mode 100644 index 0000000..e657b1d --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/api/Client.scala @@ -0,0 +1,98 @@ +package app.softnetwork.payment.api + +import akka.actor.typed.ActorSystem +import akka.grpc.GrpcClientSettings +import app.softnetwork.api.server.client.{GrpcClient, GrpcClientFactory} +import app.softnetwork.payment.api.config.SoftPayClientSettings + +import scala.concurrent.Future + +trait Client extends GrpcClient { + + implicit lazy val grpcClient: ClientServiceApiClient = + ClientServiceApiClient( + GrpcClientSettings.fromConfig(name) + ) + + lazy val settings: SoftPayClientSettings = SoftPayClientSettings(system) + + def generateClientTokens( + scope: Option[String] = None + ): Future[Either[String, Option[Tokens]]] = { + import settings._ + grpcClient + .generateClientTokens() + .invoke( + GenerateClientTokensRequest(clientId, apiKey, scope) + ) map (response => + response.clientTokens match { + case r: ClientTokensResponse.ClientTokens.Tokens => + Right(r.tokens) + case error: ClientTokensResponse.ClientTokens.Error => Left(error.value) + } + ) + } + + def refreshClientTokens(refreshToken: String): Future[Either[String, Option[Tokens]]] = { + grpcClient + .refreshClientTokens() + .invoke( + RefreshClientTokensRequest(refreshToken) + ) map (response => + response.clientTokens match { + case r: ClientTokensResponse.ClientTokens.Tokens => + Right(r.tokens) + case error: ClientTokensResponse.ClientTokens.Error => Left(error.value) + } + ) + } + + def signUpClient( + principal: String, + credentials: Array[Char], + providerId: String, + providerApiKey: Array[Char], + providerType: Option[ProviderType] + ): Future[Either[String, ClientCreated]] = { + grpcClient + .signUpClient() + .invoke( + SignUpClientRequest( + principal, + credentials.mkString, + providerId, + providerApiKey.mkString, + providerType.getOrElse(ProviderType.MANGOPAY) + ) + ) map (response => + response.signup match { + case r: SignUpClientResponse.Signup.Client => + Right(r.value) + case error: SignUpClientResponse.Signup.Error => Left(error.value) + } + ) + } + + def activateClient(token: String): Future[Either[String, Boolean]] = { + grpcClient + .activateClient() + .invoke( + ActivateClientRequest(token) + ) map (response => + response.activation match { + case r: ActivateClientResponse.Activation.Activated => Right(r.value) + case error: ActivateClientResponse.Activation.Error => Left(error.value) + } + ) + } +} + +object Client extends GrpcClientFactory[Client] { + override val name: String = "ClientService" + override def init(sys: ActorSystem[_]): Client = { + new Client { + override implicit lazy val system: ActorSystem[_] = sys + val name: String = Client.name + } + } +} diff --git a/client/src/main/scala/app/softnetwork/payment/api/PaymentClient.scala b/client/src/main/scala/app/softnetwork/payment/api/PaymentClient.scala new file mode 100644 index 0000000..c25a828 --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/api/PaymentClient.scala @@ -0,0 +1,300 @@ +package app.softnetwork.payment.api + +import akka.actor.typed.ActorSystem +import akka.grpc.GrpcClientSettings +import akka.grpc.scaladsl.SingleResponseRequestBuilder +import app.softnetwork.api.server.client.{GrpcClient, GrpcClientFactory} +import app.softnetwork.payment.api.config.SoftPayClientSettings +import app.softnetwork.payment.api.serialization._ +import app.softnetwork.payment.model.{ + BankAccountOwner, + LegalUserDetails, + PaymentAccount, + RecurringPayment +} + +import java.util.Date +import scala.concurrent.Future + +trait PaymentClient extends GrpcClient { + implicit lazy val grpcClient: PaymentServiceApiClient = + PaymentServiceApiClient( + GrpcClientSettings.fromConfig(name) + ) + + lazy val settings: SoftPayClientSettings = SoftPayClientSettings(system) + + private lazy val generatedToken: String = settings.generateToken() + + private def withAuthorization[Req, Res]( + single: SingleResponseRequestBuilder[Req, Res], + token: Option[String] + ): SingleResponseRequestBuilder[Req, Res] = { + oauth2(single, token.getOrElse(generatedToken)) + } + + def createOrUpdatePaymentAccount( + paymentAccount: PaymentAccount, + token: Option[String] = None + ): Future[Boolean] = { + withAuthorization( + grpcClient.createOrUpdatePaymentAccount(), + token + ) + .invoke( + CreateOrUpdatePaymentAccountRequest( + Some(paymentAccount.withClientId(settings.clientId)) + ) + ) map (_.succeeded) + } + + def payInWithCardPreAuthorized( + preAuthorizationId: String, + creditedAccount: String, + debitedAmount: Option[Int], + token: Option[String] = None + ): Future[TransactionResponse] = { + withAuthorization( + grpcClient.payInWithCardPreAuthorized(), + token + ) + .invoke( + PayInWithCardPreAuthorizedRequest( + preAuthorizationId, + creditedAccount, + debitedAmount, + settings.clientId + ) + ) + } + + def cancelPreAuthorization( + orderUuid: String, + cardPreAuthorizedTransactionId: String, + token: Option[String] = None + ): Future[Option[Boolean]] = { + withAuthorization( + grpcClient.cancelPreAuthorization(), + token + ) + .invoke( + CancelPreAuthorizationRequest(orderUuid, cardPreAuthorizedTransactionId, settings.clientId) + ) map (_.preAuthorizationCanceled) + } + + def refund( + orderUuid: String, + payInTransactionId: String, + refundAmount: Int, + currency: String, + reasonMessage: String, + initializedByClient: Boolean, + token: Option[String] = None + ): Future[TransactionResponse] = { + withAuthorization( + grpcClient.refund(), + token + ) + .invoke( + RefundRequest( + orderUuid, + payInTransactionId, + refundAmount, + currency, + reasonMessage, + initializedByClient, + settings.clientId + ) + ) + } + + def payOut( + orderUuid: String, + creditedAccount: String, + creditedAmount: Int, + feesAmount: Int, + currency: String, + externalReference: Option[String], + token: Option[String] = None + ): Future[TransactionResponse] = { + withAuthorization( + grpcClient.payOut(), + token + ) + .invoke( + PayOutRequest( + orderUuid, + creditedAccount, + creditedAmount, + feesAmount, + currency, + externalReference, + settings.clientId + ) + ) + } + + def transfer( + orderUuid: Option[String], + debitedAccount: String, + creditedAccount: String, + debitedAmount: Int, + feesAmount: Int, + currency: String, + payOutRequired: Boolean, + externalReference: Option[String], + token: Option[String] = None + ): Future[TransferResponse] = { + withAuthorization( + grpcClient.transfer(), + token + ) + .invoke( + TransferRequest( + orderUuid, + debitedAccount, + creditedAccount, + debitedAmount, + feesAmount, + currency, + payOutRequired, + externalReference, + settings.clientId + ) + ) + } + + def directDebit( + creditedAccount: String, + debitedAmount: Int, + feesAmount: Int, + currency: String, + statementDescriptor: String, + externalReference: Option[String], + token: Option[String] = None + ): Future[TransactionResponse] = { + withAuthorization( + grpcClient.directDebit(), + token + ) + .invoke( + DirectDebitRequest( + creditedAccount, + debitedAmount, + feesAmount, + currency, + statementDescriptor, + externalReference, + settings.clientId + ) + ) + } + + def loadDirectDebitTransaction( + directDebitTransactionId: String, + token: Option[String] = None + ): Future[TransactionResponse] = { + withAuthorization( + grpcClient.loadDirectDebitTransaction(), + token + ) + .invoke( + LoadDirectDebitTransactionRequest(directDebitTransactionId, settings.clientId) + ) + } + + def registerRecurringPayment( + debitedAccount: String, + firstDebitedAmount: Int, + firstFeesAmount: Int, + currency: String, + `type`: RecurringPayment.RecurringPaymentType, + startDate: Option[Date], + endDate: Option[Date], + frequency: Option[RecurringPayment.RecurringPaymentFrequency], + fixedNextAmount: Option[Boolean], + nextDebitedAmount: Option[Int], + nextFeesAmount: Option[Int], + statementDescriptor: Option[String], + externalReference: Option[String], + token: Option[String] = None + ): Future[Option[String]] = { + withAuthorization( + grpcClient.registerRecurringPayment(), + token + ) + .invoke( + RegisterRecurringPaymentRequest( + debitedAccount, + firstDebitedAmount, + firstFeesAmount, + currency, + `type`, + startDate, + endDate, + frequency match { + case Some(f) => f + case _ => + RegisterRecurringPaymentRequest.RecurringPaymentFrequency.UNKNOWN_PAYMENT_FREQUENCY + }, + fixedNextAmount, + nextDebitedAmount, + nextFeesAmount, + statementDescriptor, + externalReference, + settings.clientId + ) + ) map (_.recurringPaymentRegistrationId) + } + + def cancelMandate(externalUuid: String, token: Option[String] = None): Future[Boolean] = { + withAuthorization( + grpcClient.cancelMandate(), + token + ) + .invoke(CancelMandateRequest(externalUuid)) map (_.succeeded) + } + + def loadBankAccountOwner( + externalUuid: String, + token: Option[String] = None + ): Future[BankAccountOwner] = { + withAuthorization( + grpcClient.loadBankAccountOwner(), + token + ) + .invoke(LoadBankAccountOwnerRequest(externalUuid, settings.clientId)) map (response => + BankAccountOwner(response.ownerName, response.ownerAddress) + ) + } + + def loadLegalUserDetails( + externalUuid: String, + token: Option[String] = None + ): Future[LegalUserDetails] = { + withAuthorization( + grpcClient.loadLegalUser(), + token + ) + .invoke(LoadLegalUserRequest(externalUuid, settings.clientId)) map (response => + LegalUserDetails( + response.legalUserType, + response.legalName, + response.siret, + response.legalRepresentativeAddress, + response.headQuartersAddress + ) + ) + } + +} + +object PaymentClient extends GrpcClientFactory[PaymentClient] { + override val name: String = "PaymentService" + override def init(sys: ActorSystem[_]): PaymentClient = { + new PaymentClient { + override implicit lazy val system: ActorSystem[_] = sys + val name: String = PaymentClient.name + } + } +} diff --git a/client/src/main/scala/app/softnetwork/payment/api/config/ApiKeys.scala b/client/src/main/scala/app/softnetwork/payment/api/config/ApiKeys.scala new file mode 100644 index 0000000..122a1b3 --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/api/config/ApiKeys.scala @@ -0,0 +1,48 @@ +package app.softnetwork.payment.api.config + +import java.nio.file.Paths + +object ApiKeys { + + private[this] lazy val filePath: String = { + val config = SoftPayClientSettings.SOFT_PAY_HOME + "/config" + Paths.get(config).toFile.mkdirs() + config + "/apiKeys.conf" + } + + private[this] def write(apiKeys: Map[String, String]): Unit = { + val file = Paths.get(filePath).toFile + file.createNewFile() + val apiKeysWriter = new java.io.BufferedWriter(new java.io.FileWriter(file)) + apiKeys.foreach { case (clientId, apiKey) => + apiKeysWriter.append( + s"""$clientId=$apiKey + |""".stripMargin + ) + } + apiKeysWriter.close() + } + + def list(): Map[String, String] = { + val file = Paths.get(filePath).toFile + file.createNewFile() + import scala.io.Source + val source = Source + .fromFile(filePath) + val apiKeys = + source.getLines + .filter(line => line.nonEmpty) + .map(line => line.split("=")) + .map { case Array(clientId, apiKey) => clientId.trim -> apiKey.trim } + .toMap + source.close() + apiKeys //.keys.map(clientId => ApiKey(clientId, Some(apiKeys(clientId)))).toSeq + } + + def +(clientId: String, apiKey: String): Unit = write(list() + (clientId -> apiKey)) + + def -(clientId: String): Unit = write(list() - clientId) + + def get(clientId: String): Option[String] = list().get(clientId) + +} diff --git a/client/src/main/scala/app/softnetwork/payment/api/config/SoftPayClientSettings.scala b/client/src/main/scala/app/softnetwork/payment/api/config/SoftPayClientSettings.scala new file mode 100644 index 0000000..dc25896 --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/api/config/SoftPayClientSettings.scala @@ -0,0 +1,73 @@ +package app.softnetwork.payment.api.config + +import akka.actor.typed.ActorSystem +import com.typesafe.config.{Config, ConfigFactory} +import org.softnetwork.session.model.JwtClaims + +import java.nio.file.{Path, Paths} + +case class SoftPayClientSettings(clientId: String, apiKey: String) { + def generateToken(): String = + JwtClaims.newSession + .withClientId(clientId) + .encode(clientId, apiKey) + + def write(): Unit = { + ApiKeys.+(clientId, apiKey) + select() + } + + private[payment] def select(): Unit = { + val config = SoftPayClientSettings.SOFT_PAY_HOME + "/config" + Paths.get(config).toFile.mkdirs() + val application = Paths.get(config + "/application.conf").toFile + application.createNewFile() + val applicationWriter = new java.io.BufferedWriter(new java.io.FileWriter(application)) + applicationWriter.write( + s"""client-id = "$clientId" + |api-key = "$apiKey" + |""".stripMargin + ) + applicationWriter.close() + } + +} + +object SoftPayClientSettings { + lazy val SOFT_PAY_HOME: String = + sys.env.getOrElse( + "SOFT_PAY_HOME", + Option(System.getProperty("user.home") + "/soft-pay").getOrElse(".") + ) + + def apply(system: ActorSystem[_]): SoftPayClientSettings = { + val clientConfigFile: Path = Paths.get(s"$SOFT_PAY_HOME/config/application.conf") + val systemConfig = system.settings.config.getConfig("payment") + val clientConfig: Config = { + if ( + clientConfigFile.toFile + .exists() && (!systemConfig.hasPath("test") || !systemConfig.getBoolean("test")) + ) { + ConfigFactory + .parseFile(clientConfigFile.toFile) + .withFallback(systemConfig) + } else { + systemConfig + } + } + SoftPayClientSettings( + clientId = clientConfig.getString("client-id"), + apiKey = clientConfig.getString("api-key") + ) + } + + def select(clientId: String): Option[SoftPayClientSettings] = { + ApiKeys.list().get(clientId) match { + case Some(apiKey) => + val softPayClientSettings = SoftPayClientSettings(clientId, apiKey) + softPayClientSettings.select() + Some(softPayClientSettings) + case None => None + } + } +} diff --git a/common/src/main/scala/app/softnetwork/payment/api/api.scala b/client/src/main/scala/app/softnetwork/payment/api/serialization/package.scala similarity index 81% rename from common/src/main/scala/app/softnetwork/payment/api/api.scala rename to client/src/main/scala/app/softnetwork/payment/api/serialization/package.scala index 9e30a71..a15a425 100644 --- a/common/src/main/scala/app/softnetwork/payment/api/api.scala +++ b/client/src/main/scala/app/softnetwork/payment/api/serialization/package.scala @@ -1,11 +1,10 @@ -package app.softnetwork.payment +package app.softnetwork.payment.api -import app.softnetwork.payment.model.RecurringPayment +import app.softnetwork.payment.model.{LegalUser, RecurringPayment} import scala.language.implicitConversions -package object api { - +package object serialization { implicit def recurringPaymentTypeToRegisterRecurringPaymentType( `type`: RecurringPayment.RecurringPaymentType ): RegisterRecurringPaymentRequest.RecurringPaymentType = { @@ -77,4 +76,25 @@ package object api { case _ => None } } + + implicit def LegalUserTypeToLegalResponseUserType( + legalUserType: LegalUser.LegalUserType + ): LegalUserType = { + legalUserType match { + case LegalUser.LegalUserType.BUSINESS => LegalUserType.BUSINESS + case LegalUser.LegalUserType.SOLETRADER => LegalUserType.SOLETRADER + case LegalUser.LegalUserType.ORGANIZATION => LegalUserType.ORGANIZATION + } + } + + implicit def LegalResponseUserTypeToLegalUserType( + legalUserType: LegalUserType + ): LegalUser.LegalUserType = { + legalUserType match { + case LegalUserType.BUSINESS => LegalUser.LegalUserType.BUSINESS + case LegalUserType.SOLETRADER => LegalUser.LegalUserType.SOLETRADER + case LegalUserType.ORGANIZATION => LegalUser.LegalUserType.ORGANIZATION + } + } + } diff --git a/client/src/main/scala/app/softnetwork/payment/cli/Cmd.scala b/client/src/main/scala/app/softnetwork/payment/cli/Cmd.scala new file mode 100644 index 0000000..d923572 --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/cli/Cmd.scala @@ -0,0 +1,34 @@ +package app.softnetwork.payment.cli + +import akka.actor.typed.ActorSystem +import scopt.OParser + +import scala.concurrent.Future + +trait Cmd[T] { + + def name: String + + private[cli] def shell: String = Main.shell + + def parser: OParser[Unit, T] + + private[cli] def usage(): String = OParser.usage(parser) + + def parse(args: Seq[String]): Option[T] + + final def run( + args: Seq[String] + )(implicit system: ActorSystem[_]): Future[(Int, Option[String])] = { + parse(args) match { + case Some(config) => run(config) + case None => + Future.successful( + (1, Some(s"ERROR: Invalid arguments for command --> $shell $name\n${usage()}")) + ) + } + } + + def run(config: T)(implicit system: ActorSystem[_]): Future[(Int, Option[String])] + +} diff --git a/client/src/main/scala/app/softnetwork/payment/cli/Main.scala b/client/src/main/scala/app/softnetwork/payment/cli/Main.scala new file mode 100644 index 0000000..594ce04 --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/cli/Main.scala @@ -0,0 +1,94 @@ +package app.softnetwork.payment.cli + +import akka.actor.typed.ActorSystem +import akka.actor.typed.scaladsl.Behaviors +import app.softnetwork.concurrent.Completion +import app.softnetwork.payment.SoftpayBuildInfo +import app.softnetwork.payment.cli.activate.ActivateClientCmd +import app.softnetwork.payment.cli.clients.ClientsCmd +import app.softnetwork.payment.cli.signup.SignUpClientCmd +import app.softnetwork.payment.cli.tokens.TokensCmd +import com.typesafe.scalalogging.StrictLogging + +import scala.util.{Failure, Success} + +object Main extends StrictLogging { + + def shell: String = "softpay" + + def main(args: Array[String]): Unit = { + implicit def system: ActorSystem[_] = ActorSystem(Behaviors.empty, "PaymentClient") + new Main().run(args) + } +} + +class Main extends Completion with StrictLogging { + private[cli] val cmds: List[Cmd[_]] = List( + SignUpClientCmd, + ActivateClientCmd, + TokensCmd, + ClientsCmd + ) + + private[cli] def printUsage(): Unit = { + // scalastyle:off println + println(s"Softpay Version ${SoftpayBuildInfo.version}") + println("Usage:") + println(s"\t${Main.shell} [command]") + println("Available commands =>") + cmds.foreach { cmd => + println(s"\t${cmd.name}") + } + } + + private[cli] def printUsage(cmd: String): Unit = { + // scalastyle:off println + cmds.find(_.name == cmd) match { + case None => + println(s"ERROR: Unknown command --> $cmd") + case Some(cmd) => + println(cmd.usage()) + } + } + // scalastyle:on println + + private[cli] def help(args: List[String]): Unit = { + args match { + case Nil | "help" :: Nil => + printUsage() + System.exit(0) + case "help" :: command :: _ => + printUsage(command) + System.exit(0) + case command :: "help" :: Nil => + printUsage(command) + System.exit(0) + case _ => + } + } + + def run(args: Array[String])(implicit system: ActorSystem[_]): Unit = { + help(args.toList) + args.toList match { + case command :: list => + cmds.find(_.name == command) match { + case None => + println(s"ERROR: Unknown command --> $command") + printUsage() + System.exit(1) + case Some(cmd) => + cmd.run(list) complete () match { + case Success((exit, message)) => + message.foreach(println) + System.exit(exit) + case Failure(f) => + logger.error(s"Failed to run command ${cmd.name}", f) + System.exit(1) + } + } + case _ => + printUsage() + System.exit(1) + } + } +} diff --git a/client/src/main/scala/app/softnetwork/payment/cli/activate/ActivateClientCmd.scala b/client/src/main/scala/app/softnetwork/payment/cli/activate/ActivateClientCmd.scala new file mode 100644 index 0000000..33e4f96 --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/cli/activate/ActivateClientCmd.scala @@ -0,0 +1,50 @@ +package app.softnetwork.payment.cli.activate + +import akka.actor.typed.ActorSystem +import app.softnetwork.payment.api.Client +import app.softnetwork.payment.cli.Cmd +import scopt.OParser + +import scala.concurrent.{ExecutionContext, Future} + +object ActivateClientCmd extends Cmd[ActivateClientConfig] { + + val name: String = "activate" + + val parser: OParser[Unit, ActivateClientConfig] = { + val builder = OParser.builder[ActivateClientConfig] + import builder._ + OParser.sequence( + programName(s"$shell $name"), + head(shell, name, "[options]"), + opt[String]('t', "token") + .action((x, c) => c.copy(token = x)) + .text("token") + .required() + ) + } + + def parse(args: Seq[String]): Option[ActivateClientConfig] = { + OParser.parse(parser, args, ActivateClientConfig()) + } + + override def run( + config: ActivateClientConfig + )(implicit system: ActorSystem[_]): Future[(Int, Option[String])] = { + implicit val ec: ExecutionContext = system.executionContext + val client = Client(system) + client.activateClient(config.token) map { + case Right(activated) => + if (activated) (0, Some("Client activated successfully!")) + else (1, Some("Client activation failed!")) + case Left(error) => + ( + 1, + Some(s""" + |Client activation failed! + |$error + |""".stripMargin) + ) + } + } +} diff --git a/client/src/main/scala/app/softnetwork/payment/cli/activate/ActivateClientConfig.scala b/client/src/main/scala/app/softnetwork/payment/cli/activate/ActivateClientConfig.scala new file mode 100644 index 0000000..3d7da4a --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/cli/activate/ActivateClientConfig.scala @@ -0,0 +1,3 @@ +package app.softnetwork.payment.cli.activate + +case class ActivateClientConfig(token: String = "") diff --git a/client/src/main/scala/app/softnetwork/payment/cli/clients/ClientsCmd.scala b/client/src/main/scala/app/softnetwork/payment/cli/clients/ClientsCmd.scala new file mode 100644 index 0000000..45fb143 --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/cli/clients/ClientsCmd.scala @@ -0,0 +1,158 @@ +package app.softnetwork.payment.cli.clients + +import akka.actor.typed.ActorSystem +import app.softnetwork.payment.api.config.{ApiKeys, SoftPayClientSettings} +import app.softnetwork.payment.cli.Cmd +import scopt.OParser + +import scala.concurrent.Future + +object ClientsCmd extends Cmd[ClientsConfig] { + + val name: String = "clients" + + val parser: OParser[Unit, ClientsConfig] = { + val builder = OParser.builder[ClientsConfig] + import builder._ + OParser.sequence( + programName(s"$shell $name"), + head(shell, name, "[options]"), + cmd("list") + .action((_, c) => c.copy(subCommand = ClientsSubCommand.List)) + .text("list all clients"), + cmd("get") + .action((_, c) => c.copy(subCommand = ClientsSubCommand.Get)) + .text("get client by id") + .children( + opt[String]('i', "clientId") + .action((x, c) => c.copy(clientId = Some(x))) + .text("client Id") + .required() + ), + cmd("remove") + .action((_, c) => c.copy(subCommand = ClientsSubCommand.Remove)) + .text("remove client by id") + .children( + opt[String]('i', "clientId") + .action((x, c) => c.copy(clientId = Some(x))) + .text("client Id") + .required() + ), + cmd("add") + .action((_, c) => c.copy(subCommand = ClientsSubCommand.Add)) + .text("add api key") + .children( + opt[String]('i', "clientId") + .action((x, c) => c.copy(clientId = Some(x))) + .text("client Id") + .required(), + opt[String]('s', "apiKey") + .action((x, c) => c.copy(apiKey = Some(x))) + .text("api Key") + .required() + ), + cmd("set") + .action((_, c) => c.copy(subCommand = ClientsSubCommand.Set)) + .text("set client by id") + .children( + opt[String]('i', "clientId") + .action((x, c) => c.copy(clientId = Some(x))) + .text("client Id") + .required() + ) + ) + } + + def parse(args: Seq[String]): Option[ClientsConfig] = { + OParser.parse(parser, args, ClientsConfig()) + } + + override def run( + config: ClientsConfig + )(implicit system: ActorSystem[_]): Future[(Int, Option[String])] = { + config.subCommand match { + case ClientsSubCommand.List => + Future.successful( + ( + 0, + Some(s""" + |Available clients: + |\t${ApiKeys.list().filter(_._2.trim.nonEmpty).keys.mkString("\n\t")} + |""".stripMargin) + ) + ) + case ClientsSubCommand.Set => + config.clientId match { + case Some(clientId) => + Future.successful(SoftPayClientSettings.select(clientId) match { + case Some(softPayClientSettings) => + ( + 0, + Some(s""" + |Client $clientId selected successfully! + |\tapiKey: ${softPayClientSettings.apiKey} + |""".stripMargin) + ) + case None => + ( + 1, + Some(s""" + |apiKey not found for $clientId! + |""".stripMargin) + ) + }) + } + case ClientsSubCommand.Get => + config.clientId match { + case Some(clientId) => + Future.successful(ApiKeys.get(clientId) match { + case Some(apiKey) => + ( + 0, + Some(s""" + |Client $clientId loaded successfully! + |\tapiKey: $apiKey + |""".stripMargin) + ) + case None => + ( + 1, + Some(s""" + |apiKey not found for $clientId! + |""".stripMargin) + ) + }) + } + case ClientsSubCommand.Add => + config.clientId match { + case Some(clientId) => + ApiKeys.+(clientId, config.apiKey.getOrElse("")) match { + case _ => + Future.successful( + ( + 0, + Some(s""" + |Client $clientId added successfully! + |""".stripMargin) + ) + ) + } + } + case ClientsSubCommand.Remove => + config.clientId match { + case Some(clientId) => + ApiKeys.-(clientId) match { + case _ => + Future.successful( + ( + 0, + Some(s""" + |Client $clientId removed successfully! + |""".stripMargin) + ) + ) + } + } + } + } +} diff --git a/client/src/main/scala/app/softnetwork/payment/cli/clients/ClientsConfig.scala b/client/src/main/scala/app/softnetwork/payment/cli/clients/ClientsConfig.scala new file mode 100644 index 0000000..d12062a --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/cli/clients/ClientsConfig.scala @@ -0,0 +1,12 @@ +package app.softnetwork.payment.cli.clients + +object ClientsSubCommand extends Enumeration { + type ClientsSubCommand = Value + val Empty, List, Get, Set, Add, Remove = Value +} + +case class ClientsConfig( + subCommand: ClientsSubCommand.Value = ClientsSubCommand.Empty, + clientId: Option[String] = None, + apiKey: Option[String] = None +) diff --git a/client/src/main/scala/app/softnetwork/payment/cli/signup/SignUpClientCmd.scala b/client/src/main/scala/app/softnetwork/payment/cli/signup/SignUpClientCmd.scala new file mode 100644 index 0000000..efd3c01 --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/cli/signup/SignUpClientCmd.scala @@ -0,0 +1,89 @@ +package app.softnetwork.payment.cli.signup + +import akka.actor.typed.ActorSystem +import app.softnetwork.payment.api.config.SoftPayClientSettings +import app.softnetwork.payment.api.{Client, ProviderType} +import app.softnetwork.payment.cli.Cmd +import org.json4s.Formats +import scopt.OParser + +import scala.concurrent.{ExecutionContext, Future} + +object SignUpClientCmd extends Cmd[SignUpClientConfig] { + + val name: String = "signup" + + val parser: OParser[Unit, SignUpClientConfig] = { + val builder = OParser.builder[SignUpClientConfig] + import builder._ + OParser.sequence( + programName(s"$shell $name"), + head(shell, name, "[options]"), + opt[String]('p', "principal") + .action((x, c) => c.copy(principal = x)) + .text("principal") + .required(), + opt[String]('c', "credentials") + .action((x, c) => c.copy(credentials = x.toCharArray)) + .text("credentials") + .required() + /*.validate(x => + import app.softnetwork.account.config.AccountSettings.passwordRules + passwordRules().validate(x) match { + case Right(_) => success + case Left(error) => failure(error.mkString(", ")) + } + )*/, + opt[String]('i', "providerId") + .action((x, c) => c.copy(providerId = x)) + .text("payment provider Id") + .required(), + opt[String]('k', "providerApiKey") + .action((x, c) => c.copy(providerApiKey = x.toCharArray)) + .text("payment provider Api Key") + .required(), + opt[String]('t', "providerType") + .action((x, c) => c.copy(providerType = ProviderType.fromName(x.toUpperCase))) + .text("optional payment provider type - default is 'MangoPay'") + .optional() + ) + } + + def parse(args: Seq[String]): Option[SignUpClientConfig] = { + OParser.parse(parser, args, SignUpClientConfig()) + } + + override def run( + config: SignUpClientConfig + )(implicit system: ActorSystem[_]): Future[(Int, Option[String])] = { + implicit val ec: ExecutionContext = system.executionContext + val client = Client(system) + implicit val formats: Formats = org.json4s.DefaultFormats + client.signUpClient( + config.principal, + config.credentials, + config.providerId, + config.providerApiKey, + config.providerType + ) map { + case Right(client) => + SoftPayClientSettings(client.clientId, client.clientSecret).write() + val json = org.json4s.jackson.Serialization.writePretty(client) + ( + 0, + Some(s""" + |Client registered successfully! + |$json + |""".stripMargin) + ) + case Left(error) => + ( + 1, + Some(s""" + |Client registration failed! + |$error + |""".stripMargin) + ) + } + } +} diff --git a/client/src/main/scala/app/softnetwork/payment/cli/signup/SignUpClientConfig.scala b/client/src/main/scala/app/softnetwork/payment/cli/signup/SignUpClientConfig.scala new file mode 100644 index 0000000..23ac24f --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/cli/signup/SignUpClientConfig.scala @@ -0,0 +1,11 @@ +package app.softnetwork.payment.cli.signup + +import app.softnetwork.payment.api.ProviderType + +case class SignUpClientConfig( + principal: String = "", + credentials: Array[Char] = Array.emptyCharArray, + providerId: String = "", + providerApiKey: Array[Char] = Array.emptyCharArray, + providerType: Option[ProviderType] = Some(ProviderType.MANGOPAY) +) diff --git a/client/src/main/scala/app/softnetwork/payment/cli/tokens/TokensCmd.scala b/client/src/main/scala/app/softnetwork/payment/cli/tokens/TokensCmd.scala new file mode 100644 index 0000000..e222eff --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/cli/tokens/TokensCmd.scala @@ -0,0 +1,83 @@ +package app.softnetwork.payment.cli.tokens + +import akka.actor.typed.ActorSystem +import app.softnetwork.payment.api.Client +import app.softnetwork.payment.cli.Cmd +import org.json4s.Formats +import scopt.OParser + +import scala.concurrent.{ExecutionContext, Future} + +object TokensCmd extends Cmd[TokensConfig] { + + val name: String = "tokens" + + val parser: OParser[Unit, TokensConfig] = { + val builder = OParser.builder[TokensConfig] + import builder._ + OParser.sequence( + programName(s"$shell $name"), + head(shell, name, "[options]"), + note("Command to generate/refresh tokens"), + opt[String]('r', "refreshToken") + .action((x, c) => c.copy(refreshToken = Some(x))) + .text("optional refresh token") + .optional() + ) + } + + def parse(args: Seq[String]): Option[TokensConfig] = { + OParser.parse(parser, args, TokensConfig()) + } + + def run( + config: TokensConfig + )(implicit system: ActorSystem[_]): Future[(Int, Option[String])] = { + implicit val ec: ExecutionContext = system.executionContext + val client = Client(system) + implicit val formats: Formats = org.json4s.DefaultFormats + config.refreshToken match { + case Some(refreshToken) => + client.refreshClientTokens(refreshToken) map { + case Right(tokens) => + val json = org.json4s.jackson.Serialization.writePretty(tokens) + ( + 0, + Some(s""" + |Tokens refreshed successfully! + |$json + |""".stripMargin) + ) + case Left(error) => + ( + 1, + Some(s""" + |Tokens refresh failed! + |$error + |""".stripMargin) + ) + } + case None => + client.generateClientTokens() map { + case Right(tokens) => + val json = org.json4s.jackson.Serialization.writePretty(tokens) + ( + 0, + Some(s""" + |Tokens generated successfully! + |$json + |""".stripMargin) + ) + case Left(error) => + ( + 1, + Some(s""" + |Tokens generation failed! + |$error + |""".stripMargin) + ) + } + } + } + +} diff --git a/client/src/main/scala/app/softnetwork/payment/cli/tokens/TokensConfig.scala b/client/src/main/scala/app/softnetwork/payment/cli/tokens/TokensConfig.scala new file mode 100644 index 0000000..f618c53 --- /dev/null +++ b/client/src/main/scala/app/softnetwork/payment/cli/tokens/TokensConfig.scala @@ -0,0 +1,5 @@ +package app.softnetwork.payment.cli.tokens + +case class TokensConfig( + refreshToken: Option[String] = None +) diff --git a/common/src/main/scala/app/softnetwork/payment/model/AddressDecorator.scala b/client/src/main/scala/app/softnetwork/payment/model/AddressDecorator.scala similarity index 64% rename from common/src/main/scala/app/softnetwork/payment/model/AddressDecorator.scala rename to client/src/main/scala/app/softnetwork/payment/model/AddressDecorator.scala index cf6e1b2..f99aa90 100644 --- a/common/src/main/scala/app/softnetwork/payment/model/AddressDecorator.scala +++ b/client/src/main/scala/app/softnetwork/payment/model/AddressDecorator.scala @@ -1,5 +1,7 @@ package app.softnetwork.payment.model +import app.softnetwork.payment.model + import java.util.Locale trait AddressDecorator { self: Address => @@ -10,14 +12,15 @@ trait AddressDecorator { self: Address => country.trim.isEmpty || postalCode.trim.isEmpty - lazy val view: AddressView = AddressView(self) + lazy val view: AddressView = model.AddressView(self) override def equals(obj: Any): Boolean = obj match { case address: Address => address.addressLine.equals(addressLine) && address.city.equals(city) && address.country.equals(country) && - address.postalCode.equals(postalCode) + address.postalCode.equals(postalCode) && + address.state.getOrElse("").equals(state.getOrElse("")) case _ => super.equals(obj) } @@ -25,11 +28,17 @@ trait AddressDecorator { self: Address => new Locale(language, country.toUpperCase).getDisplayCountry } -case class AddressView(addressLine: String, city: String, postalCode: String, country: String) +case class AddressView( + addressLine: String, + city: String, + postalCode: String, + country: String, + state: Option[String] +) object AddressView { def apply(address: Address): AddressView = { import address._ - AddressView(addressLine, city, postalCode, country) + AddressView(addressLine, city, postalCode, country, state) } } diff --git a/common/src/main/scala/app/softnetwork/payment/model/BankAccountCompanion.scala b/client/src/main/scala/app/softnetwork/payment/model/BankAccountCompanion.scala similarity index 100% rename from common/src/main/scala/app/softnetwork/payment/model/BankAccountCompanion.scala rename to client/src/main/scala/app/softnetwork/payment/model/BankAccountCompanion.scala diff --git a/common/src/main/scala/app/softnetwork/payment/model/BankAccountDecorator.scala b/client/src/main/scala/app/softnetwork/payment/model/BankAccountDecorator.scala similarity index 96% rename from common/src/main/scala/app/softnetwork/payment/model/BankAccountDecorator.scala rename to client/src/main/scala/app/softnetwork/payment/model/BankAccountDecorator.scala index e3c325d..0c579d1 100644 --- a/common/src/main/scala/app/softnetwork/payment/model/BankAccountDecorator.scala +++ b/client/src/main/scala/app/softnetwork/payment/model/BankAccountDecorator.scala @@ -1,5 +1,6 @@ package app.softnetwork.payment.model +import app.softnetwork.payment.model import app.softnetwork.security._ import app.softnetwork.validation.RegexValidator import org.apache.commons.validator.routines.IBANValidator @@ -65,7 +66,7 @@ trait BankAccountDecorator { self: BankAccount => lazy val tag: String = externalUuid - lazy val view: BankAccountView = BankAccountView(self) + lazy val view: BankAccountView = model.BankAccountView(self) } case class BankAccountView( diff --git a/common/src/main/scala/app/softnetwork/payment/model/BankAccountOwner.scala b/client/src/main/scala/app/softnetwork/payment/model/BankAccountOwner.scala similarity index 100% rename from common/src/main/scala/app/softnetwork/payment/model/BankAccountOwner.scala rename to client/src/main/scala/app/softnetwork/payment/model/BankAccountOwner.scala diff --git a/common/src/main/scala/app/softnetwork/payment/model/CardDecorator.scala b/client/src/main/scala/app/softnetwork/payment/model/CardDecorator.scala similarity index 92% rename from common/src/main/scala/app/softnetwork/payment/model/CardDecorator.scala rename to client/src/main/scala/app/softnetwork/payment/model/CardDecorator.scala index 39fa261..85b8010 100644 --- a/common/src/main/scala/app/softnetwork/payment/model/CardDecorator.scala +++ b/client/src/main/scala/app/softnetwork/payment/model/CardDecorator.scala @@ -1,8 +1,9 @@ package app.softnetwork.payment.model +import app.softnetwork.payment.model + import java.text.SimpleDateFormat import java.util.Date - import scala.util.{Failure, Success, Try} trait CardDecorator { self: Card => @@ -22,7 +23,7 @@ trait CardDecorator { self: Card => .withLastName(lastName) .withBirthday(birthday) - lazy val view: CardView = CardView(self) + lazy val view: CardView = model.CardView(self) } case class CardView( diff --git a/common/src/main/scala/app/softnetwork/payment/model/KycDocumentDecorator.scala b/client/src/main/scala/app/softnetwork/payment/model/KycDocumentDecorator.scala similarity index 87% rename from common/src/main/scala/app/softnetwork/payment/model/KycDocumentDecorator.scala rename to client/src/main/scala/app/softnetwork/payment/model/KycDocumentDecorator.scala index e698291..286dbc1 100644 --- a/common/src/main/scala/app/softnetwork/payment/model/KycDocumentDecorator.scala +++ b/client/src/main/scala/app/softnetwork/payment/model/KycDocumentDecorator.scala @@ -1,7 +1,9 @@ package app.softnetwork.payment.model +import app.softnetwork.payment.model + trait KycDocumentDecorator { self: KycDocument => - lazy val view: KycDocumentView = KycDocumentView(self) + lazy val view: KycDocumentView = model.KycDocumentView(self) } case class KycDocumentView( diff --git a/common/src/main/scala/app/softnetwork/payment/model/LegalUserDecorator.scala b/client/src/main/scala/app/softnetwork/payment/model/LegalUserDecorator.scala similarity index 90% rename from common/src/main/scala/app/softnetwork/payment/model/LegalUserDecorator.scala rename to client/src/main/scala/app/softnetwork/payment/model/LegalUserDecorator.scala index 2ecb4e2..e79ce85 100644 --- a/common/src/main/scala/app/softnetwork/payment/model/LegalUserDecorator.scala +++ b/client/src/main/scala/app/softnetwork/payment/model/LegalUserDecorator.scala @@ -1,5 +1,6 @@ package app.softnetwork.payment.model +import app.softnetwork.payment.model import app.softnetwork.validation.RegexValidator import scala.util.matching.Regex @@ -16,7 +17,7 @@ trait LegalUserDecorator { self: LegalUser => lazy val uboDeclarationValidated: Boolean = !uboDeclarationRequired || uboDeclaration.exists(_.status.isUboDeclarationValidated) - lazy val view: LegalUserView = LegalUserView(self) + lazy val view: LegalUserView = model.LegalUserView(self) } object SiretValidator extends RegexValidator { @@ -27,7 +28,7 @@ case class LegalUserView( legalUserType: LegalUser.LegalUserType, legalName: String, siret: String, - legalRepresentative: PaymentUserView, + legalRepresentative: NaturalUserView, legalRepresentativeAddress: AddressView, headQuartersAddress: AddressView, uboDeclaration: Option[UboDeclarationView] = None, diff --git a/common/src/main/scala/app/softnetwork/payment/model/LegalUserDetails.scala b/client/src/main/scala/app/softnetwork/payment/model/LegalUserDetails.scala similarity index 100% rename from common/src/main/scala/app/softnetwork/payment/model/LegalUserDetails.scala rename to client/src/main/scala/app/softnetwork/payment/model/LegalUserDetails.scala diff --git a/common/src/main/scala/app/softnetwork/payment/model/PaymentUserCompanion.scala b/client/src/main/scala/app/softnetwork/payment/model/NaturalUserCompanion.scala similarity index 84% rename from common/src/main/scala/app/softnetwork/payment/model/PaymentUserCompanion.scala rename to client/src/main/scala/app/softnetwork/payment/model/NaturalUserCompanion.scala index af51c35..95cefd6 100644 --- a/common/src/main/scala/app/softnetwork/payment/model/PaymentUserCompanion.scala +++ b/client/src/main/scala/app/softnetwork/payment/model/NaturalUserCompanion.scala @@ -1,6 +1,6 @@ package app.softnetwork.payment.model -trait PaymentUserCompanion { +trait NaturalUserCompanion { def apply( firstName: String, lastName: String, @@ -8,8 +8,8 @@ trait PaymentUserCompanion { nationality: Option[String], birthday: String, countryOfResidence: Option[String] - ): PaymentUser = { - PaymentUser.defaultInstance + ): NaturalUser = { + NaturalUser.defaultInstance .withFirstName(firstName) .withLastName(lastName) .withEmail(email) diff --git a/common/src/main/scala/app/softnetwork/payment/model/PaymentUserDecorator.scala b/client/src/main/scala/app/softnetwork/payment/model/NaturalUserDecorator.scala similarity index 60% rename from common/src/main/scala/app/softnetwork/payment/model/PaymentUserDecorator.scala rename to client/src/main/scala/app/softnetwork/payment/model/NaturalUserDecorator.scala index 392c373..9d6e13c 100644 --- a/common/src/main/scala/app/softnetwork/payment/model/PaymentUserDecorator.scala +++ b/client/src/main/scala/app/softnetwork/payment/model/NaturalUserDecorator.scala @@ -1,12 +1,14 @@ package app.softnetwork.payment.model -trait PaymentUserDecorator { self: PaymentUser => +import app.softnetwork.payment.model + +trait NaturalUserDecorator { self: NaturalUser => lazy val externalUuidWithProfile: String = computeExternalUuidWithProfile(externalUuid, profile) - lazy val view: PaymentUserView = PaymentUserView(self) + lazy val view: NaturalUserView = NaturalUserView(self) } -case class PaymentUserView( +case class NaturalUserView( userId: Option[String] = None, firstName: String, lastName: String, @@ -16,13 +18,13 @@ case class PaymentUserView( countryOfResidence: String, externalUuid: String, profile: Option[String] = None, - paymentUserType: Option[PaymentUser.PaymentUserType] = None + naturalUserType: Option[NaturalUser.NaturalUserType] = None ) -object PaymentUserView { - def apply(paymentUser: PaymentUser): PaymentUserView = { +object NaturalUserView { + def apply(paymentUser: NaturalUser): NaturalUserView = { import paymentUser._ - PaymentUserView( + NaturalUserView( userId, firstName, lastName, @@ -32,7 +34,7 @@ object PaymentUserView { countryOfResidence, externalUuid, profile, - paymentUserType + naturalUserType ) } } diff --git a/common/src/main/scala/app/softnetwork/payment/model/PaymentAccountDecorator.scala b/client/src/main/scala/app/softnetwork/payment/model/PaymentAccountDecorator.scala similarity index 95% rename from common/src/main/scala/app/softnetwork/payment/model/PaymentAccountDecorator.scala rename to client/src/main/scala/app/softnetwork/payment/model/PaymentAccountDecorator.scala index 15e6016..6be0332 100644 --- a/common/src/main/scala/app/softnetwork/payment/model/PaymentAccountDecorator.scala +++ b/client/src/main/scala/app/softnetwork/payment/model/PaymentAccountDecorator.scala @@ -1,12 +1,13 @@ package app.softnetwork.payment.model +import app.softnetwork.payment import app.softnetwork.persistence._ import java.time.Instant trait PaymentAccountDecorator { self: PaymentAccount => - lazy val maybeUser: Option[PaymentUser] = { + lazy val maybeUser: Option[NaturalUser] = { if (user.isLegalUser) { Some(getLegalUser.legalRepresentative) } else if (user.isNaturalUser) { @@ -109,13 +110,13 @@ trait PaymentAccountDecorator { self: PaymentAccount => lazy val hasAcceptedTermsOfPSP: Boolean = !legalUser || getLegalUser.lastAcceptedTermsOfPSP.isDefined - lazy val view: PaymentAccountView = PaymentAccountView(self) + lazy val view: PaymentAccountView = payment.model.PaymentAccountView(self) } case class PaymentAccountView( createdDate: Instant, lastUpdated: Instant, - naturalUser: Option[PaymentUserView] = None, + naturalUser: Option[NaturalUserView] = None, legalUser: Option[LegalUserView] = None, cards: Seq[CardView] = Seq.empty, bankAccount: Option[BankAccountView] = None, diff --git a/common/src/main/scala/app/softnetwork/payment/model/RecurringPaymentDecorator.scala b/client/src/main/scala/app/softnetwork/payment/model/RecurringPaymentDecorator.scala similarity index 100% rename from common/src/main/scala/app/softnetwork/payment/model/RecurringPaymentDecorator.scala rename to client/src/main/scala/app/softnetwork/payment/model/RecurringPaymentDecorator.scala diff --git a/common/src/main/scala/app/softnetwork/payment/model/RefundAction.scala b/client/src/main/scala/app/softnetwork/payment/model/RefundAction.scala similarity index 100% rename from common/src/main/scala/app/softnetwork/payment/model/RefundAction.scala rename to client/src/main/scala/app/softnetwork/payment/model/RefundAction.scala diff --git a/common/src/main/scala/app/softnetwork/payment/model/TransactionCompanion.scala b/client/src/main/scala/app/softnetwork/payment/model/TransactionCompanion.scala similarity index 100% rename from common/src/main/scala/app/softnetwork/payment/model/TransactionCompanion.scala rename to client/src/main/scala/app/softnetwork/payment/model/TransactionCompanion.scala diff --git a/common/src/main/scala/app/softnetwork/payment/model/TransactionDecorator.scala b/client/src/main/scala/app/softnetwork/payment/model/TransactionDecorator.scala similarity index 100% rename from common/src/main/scala/app/softnetwork/payment/model/TransactionDecorator.scala rename to client/src/main/scala/app/softnetwork/payment/model/TransactionDecorator.scala diff --git a/common/src/main/scala/app/softnetwork/payment/model/UboDeclarationDecorator.scala b/client/src/main/scala/app/softnetwork/payment/model/UboDeclarationDecorator.scala similarity index 100% rename from common/src/main/scala/app/softnetwork/payment/model/UboDeclarationDecorator.scala rename to client/src/main/scala/app/softnetwork/payment/model/UboDeclarationDecorator.scala diff --git a/common/src/main/scala/app/softnetwork/payment/model/UltimateBeneficialOwnerDecorator.scala b/client/src/main/scala/app/softnetwork/payment/model/UltimateBeneficialOwnerDecorator.scala similarity index 100% rename from common/src/main/scala/app/softnetwork/payment/model/UltimateBeneficialOwnerDecorator.scala rename to client/src/main/scala/app/softnetwork/payment/model/UltimateBeneficialOwnerDecorator.scala diff --git a/common/src/main/scala/app/softnetwork/payment/model/package.scala b/client/src/main/scala/app/softnetwork/payment/model/package.scala similarity index 100% rename from common/src/main/scala/app/softnetwork/payment/model/package.scala rename to client/src/main/scala/app/softnetwork/payment/model/package.scala diff --git a/common/build.sbt b/common/build.sbt index c1d2ff5..87d6196 100644 --- a/common/build.sbt +++ b/common/build.sbt @@ -2,13 +2,10 @@ organization := "app.softnetwork.payment" name := "payment-common" +akkaGrpcGeneratedSources := Seq(AkkaGrpc.Client) + libraryDependencies ++= Seq( - "app.softnetwork.persistence" %% "persistence-kv" % Versions.genericPersistence, - "app.softnetwork.scheduler" %% "scheduler-common" % Versions.scheduler, - "app.softnetwork.scheduler" %% "scheduler-common" % Versions.scheduler % "protobuf", - "app.softnetwork.api" %% "generic-server-api" % Versions.genericPersistence, - "app.softnetwork.protobuf" %% "scalapb-extensions" % "0.1.7", - "commons-validator" % "commons-validator" % "1.6" + "app.softnetwork.persistence" %% "persistence-kv" % Versions.genericPersistence ) Compile / unmanagedResourceDirectories += baseDirectory.value / "src/main/protobuf" diff --git a/common/src/main/protobuf/message/payment/client.proto b/common/src/main/protobuf/message/payment/client.proto new file mode 100644 index 0000000..32bb09d --- /dev/null +++ b/common/src/main/protobuf/message/payment/client.proto @@ -0,0 +1,50 @@ +syntax = "proto2"; + +import "scalapb/scalapb.proto"; +import "google/protobuf/timestamp.proto"; +import "model/payment/client.proto"; + +package app.softnetwork.payment.message.SoftPayAccountEvents; + +option (scalapb.options) = { + single_file: true + flat_package: true + import: "app.softnetwork.persistence.message._" + import: "app.softnetwork.persistence.model._" + import: "app.softnetwork.protobuf.ScalaPBTypeMappers._" + import: "app.softnetwork.serialization._" + import: "app.softnetwork.account.message._" + import: "app.softnetwork.payment.model._" + import: "app.softnetwork.payment.message.SoftPayAccountEvents._" + import: "app.softnetwork.payment.serialization._" + preserve_unknown_fields: false + preamble: "sealed trait SoftPayAccountEvent extends AccountEvent" +}; + +message SoftPayAccountCreatedEvent { + option (scalapb.message).extends = "ProtobufEvent"; + option (scalapb.message).extends = "SoftPayAccountEvent"; + option (scalapb.message).extends = "AccountCreatedEvent[SoftPayAccount]"; + required app.softnetwork.payment.model.SoftPayAccount document = 1; +} + +message SoftPayAccountProviderRegisteredEvent { + option (scalapb.message).extends = "ProtobufEvent"; + option (scalapb.message).extends = "SoftPayAccountEvent"; + required app.softnetwork.payment.model.SoftPayAccount.Client client = 1; + required google.protobuf.Timestamp lastUpdated = 2 [(scalapb.field).type = "java.time.Instant"]; +} + +message SoftPayAccountTokenRegisteredEvent { + option (scalapb.message).extends = "ProtobufEvent"; + option (scalapb.message).extends = "SoftPayAccountEvent"; + required app.softnetwork.payment.model.SoftPayAccount.Client client = 1; + required google.protobuf.Timestamp lastUpdated = 2 [(scalapb.field).type = "java.time.Instant"]; +} + +message SoftPayAccountTokenRefreshedEvent { + option (scalapb.message).extends = "ProtobufEvent"; + option (scalapb.message).extends = "SoftPayAccountEvent"; + required app.softnetwork.payment.model.SoftPayAccount.Client client = 1; + required google.protobuf.Timestamp lastUpdated = 2 [(scalapb.field).type = "java.time.Instant"]; +} diff --git a/common/src/main/protobuf/message/payment/payment.proto b/common/src/main/protobuf/message/payment/payment.proto index ec0b3a5..bf7eca9 100644 --- a/common/src/main/protobuf/message/payment/payment.proto +++ b/common/src/main/protobuf/message/payment/payment.proto @@ -186,6 +186,7 @@ message PayInWithCardPreAuthorizedCommandEvent{ required string preAuthorizationId = 1; required string creditedAccount = 2; optional int32 debitedAmount = 3; + optional string clientId = 4; } message RefundCommandEvent{ @@ -197,6 +198,7 @@ message RefundCommandEvent{ required string currency = 4 [default = "EUR"]; required string reasonMessage = 5; required bool initializedByClient = 6; + optional string clientId = 7; } message PayOutCommandEvent{ @@ -208,6 +210,7 @@ message PayOutCommandEvent{ required int32 feesAmount = 4; required string currency = 5 [default = "EUR"]; optional string externalReference = 6; + optional string clientId = 7; } message TransferCommandEvent{ @@ -221,6 +224,7 @@ message TransferCommandEvent{ required string currency = 6 [default = "EUR"]; required bool payOutRequired = 7 [default = true]; optional string externalReference = 8; + optional string clientId = 9; } message DirectDebitCommandEvent{ @@ -232,6 +236,7 @@ message DirectDebitCommandEvent{ required string currency = 4 [default = "EUR"]; required string statementDescriptor = 5; optional string externalReference = 6; + optional string clientId = 7; } message CreateOrUpdatePaymentAccountCommandEvent{ @@ -254,6 +259,7 @@ message RegisterRecurringPaymentCommandEvent { optional bool fixedNextAmount = 9; optional int32 nextDebitedAmount = 10; optional int32 nextFeesAmount = 11; + optional string clientId = 12; } message RecurringPaymentRegisteredEvent { @@ -269,12 +275,14 @@ message CancelPreAuthorizationCommandEvent { option (scalapb.message).extends = "PaymentCommandEvent"; required string orderUuid = 1; required string cardPreAuthorizedTransactionId = 2; + optional string clientId = 3; } message LoadDirectDebitTransactionCommandEvent { option (scalapb.message).extends = "ProtobufEvent"; option (scalapb.message).extends = "PaymentCommandEvent"; required string directDebitTransactionId = 1; + optional string clientId = 2; } message PaymentAccountCreatedOrUpdatedEvent { @@ -290,6 +298,7 @@ message CancelMandateCommandEvent { option (scalapb.message).extends = "ProtobufEvent"; option (scalapb.message).extends = "PaymentCommandEvent"; required string externalUuid = 1; + optional string clientId = 2; } message MandateCancelationFailedEvent { diff --git a/common/src/main/protobuf/model/payment/client.proto b/common/src/main/protobuf/model/payment/client.proto new file mode 100644 index 0000000..8b4b095 --- /dev/null +++ b/common/src/main/protobuf/model/payment/client.proto @@ -0,0 +1,86 @@ +syntax = "proto2"; + +import "scalapb/scalapb.proto"; +import "model/accountStatus.proto"; +import "model/auth.proto"; +import "model/payment/address.proto"; +import "google/protobuf/timestamp.proto"; + +package app.softnetwork.payment.model; + +option (scalapb.options) = { + single_file: true + flat_package: true + import: "app.softnetwork.persistence.message._" + import: "app.softnetwork.persistence.model._" + import: "app.softnetwork.account.model._" + import: "app.softnetwork.account.serialization._" + import: "app.softnetwork.protobuf.ScalaPBTypeMappers._" + import: "Protobuf._" + preserve_unknown_fields: false +}; + +message SoftPayAccount { + + message Client { + + message Provider { + + enum ProviderType{ + MOCK = -1; + MANGOPAY = 0; + STRIPE = 1; + } + + option (scalapb.message).extends = "SoftPayProviderDecorator"; + required string providerId = 1; + required string providerApiKey = 2; + required ProviderType providerType = 3 [default = MANGOPAY]; + + } + + option (scalapb.message).extends = "SoftPayClientDecorator"; + + required Provider provider = 1; + required string clientId = 2; + optional string clientApiKey = 3; + optional string name = 4; + optional string description = 5; + optional string logoUrl = 6; + optional string websiteUrl = 7; + repeated string technicalEmails = 8; + repeated string administrativeEmails = 9; + repeated string billingEmails = 10; + repeated string fraudEmails = 11; + optional string vatNumber = 12; + optional Address address = 13; + optional app.softnetwork.account.model.AccessToken accessToken = 14; + } + + option (scalapb.message).extends = "ProtobufDomainObject"; + option (scalapb.message).extends = "Account"; + option (scalapb.message).extends = "Timestamped"; + option (scalapb.message).extends = "SoftPayAccountDecorator"; + option (scalapb.message).companion_extends = "SoftPayAccountCompanion"; + required string uuid = 1; + required google.protobuf.Timestamp createdDate = 2 [(scalapb.field).type = "java.time.Instant"]; + required google.protobuf.Timestamp lastUpdated = 3 [(scalapb.field).type = "java.time.Instant"]; + required app.softnetwork.account.model.Principal principal = 4; + repeated app.softnetwork.account.model.Principal secondaryPrincipals = 6; + optional app.softnetwork.account.model.BasicAccountProfile currentProfile = 7 [(scalapb.field).type = "Profile"]; + map profiles = 8 [(scalapb.field).value_type = "Profile"]; + required string credentials = 9; + optional google.protobuf.Timestamp lastLogin = 10 [(scalapb.field).type = "java.time.Instant"]; + required int32 nbLoginFailures = 11 [default = 0]; + required app.softnetwork.account.model.AccountStatus status = 12 [default = Inactive]; + optional app.softnetwork.account.model.VerificationToken verificationToken = 13; + optional app.softnetwork.account.model.VerificationCode verificationCode = 14; + optional app.softnetwork.account.model.BasicAccountDetails details = 15 [(scalapb.field).type = "AccountDetails"]; + repeated app.softnetwork.account.model.DeviceRegistration registrations = 16; + optional google.protobuf.Timestamp lastLogout = 17 [(scalapb.field).type = "java.time.Instant"]; + optional bool anonymous = 18; + optional bool fromAnonymous = 19; + repeated app.softnetwork.account.model.Application applications = 20; + + repeated Client clients = 21; +} \ No newline at end of file diff --git a/common/src/main/resources/reference.conf b/common/src/main/resources/reference.conf index eca257d..26a5119 100644 --- a/common/src/main/resources/reference.conf +++ b/common/src/main/resources/reference.conf @@ -1,11 +1,20 @@ # Important: enable HTTP/2 in ActorSystem's config akka.http.server.preview.enable-http2 = on +softnetwork { + api { + server { + port = 9000 + root-path = "payment" + } + } +} + payment{ - baseUrl = "http://localhost/api" + baseUrl = "http://localhost:"${softnetwork.api.server.port}"/"${softnetwork.api.server.root-path} baseUrl = ${?PAYMENT_BASE_URL} - path = "payment" + path = "api" path = ${?PAYMENT_PATH} payIn-route = "payIn" @@ -28,4 +37,18 @@ payment{ event-streams { external-to-payment-account-tag = "external-to-payment-account" } -} \ No newline at end of file + +} + +auth { + baseUrl = "http://localhost:"${softnetwork.api.server.port}"/"${softnetwork.api.server.root-path} + + path = "account" + + realm = "SoftPayment" + + oauth { + path = "oauth" + } + +} diff --git a/common/src/main/scala/app/softnetwork/payment/api/PaymentClient.scala b/common/src/main/scala/app/softnetwork/payment/api/PaymentClient.scala deleted file mode 100644 index f10ea99..0000000 --- a/common/src/main/scala/app/softnetwork/payment/api/PaymentClient.scala +++ /dev/null @@ -1,203 +0,0 @@ -package app.softnetwork.payment.api - -import akka.actor.typed.ActorSystem -import akka.grpc.GrpcClientSettings -import app.softnetwork.api.server.client.{GrpcClient, GrpcClientFactory} -import app.softnetwork.payment.model.{ - BankAccountOwner, - LegalUserDetails, - PaymentAccount, - RecurringPayment -} -import app.softnetwork.payment.serialization._ - -import java.util.Date -import scala.concurrent.Future - -trait PaymentClient extends GrpcClient { - implicit lazy val grpcClient: PaymentServiceApiClient = - PaymentServiceApiClient( - GrpcClientSettings.fromConfig(name) - ) - - def createOrUpdatePaymentAccount(paymentAccount: PaymentAccount): Future[Boolean] = { - grpcClient.createOrUpdatePaymentAccount( - CreateOrUpdatePaymentAccountRequest(Some(paymentAccount)) - ) map (_.succeeded) - } - - def payInWithCardPreAuthorized( - preAuthorizationId: String, - creditedAccount: String, - debitedAmount: Option[Int] - ): Future[TransactionResponse] = { - grpcClient.payInWithCardPreAuthorized( - PayInWithCardPreAuthorizedRequest(preAuthorizationId, creditedAccount, debitedAmount) - ) - } - - def cancelPreAuthorization( - orderUuid: String, - cardPreAuthorizedTransactionId: String - ): Future[Option[Boolean]] = { - grpcClient.cancelPreAuthorization( - CancelPreAuthorizationRequest(orderUuid, cardPreAuthorizedTransactionId) - ) map (_.preAuthorizationCanceled) - } - - def refund( - orderUuid: String, - payInTransactionId: String, - refundAmount: Int, - currency: String, - reasonMessage: String, - initializedByClient: Boolean - ): Future[TransactionResponse] = { - grpcClient.refund( - RefundRequest( - orderUuid, - payInTransactionId, - refundAmount, - currency, - reasonMessage, - initializedByClient - ) - ) - } - - def payOut( - orderUuid: String, - creditedAccount: String, - creditedAmount: Int, - feesAmount: Int, - currency: String, - externalReference: Option[String] - ): Future[TransactionResponse] = { - grpcClient.payOut( - PayOutRequest( - orderUuid, - creditedAccount, - creditedAmount, - feesAmount, - currency, - externalReference - ) - ) - } - - def transfer( - orderUuid: Option[String], - debitedAccount: String, - creditedAccount: String, - debitedAmount: Int, - feesAmount: Int, - currency: String, - payOutRequired: Boolean, - externalReference: Option[String] - ): Future[TransferResponse] = { - grpcClient.transfer( - TransferRequest( - orderUuid, - debitedAccount, - creditedAccount, - debitedAmount, - feesAmount, - currency, - payOutRequired, - externalReference - ) - ) - } - - def directDebit( - creditedAccount: String, - debitedAmount: Int, - feesAmount: Int, - currency: String, - statementDescriptor: String, - externalReference: Option[String] - ): Future[TransactionResponse] = { - grpcClient.directDebit( - DirectDebitRequest( - creditedAccount, - debitedAmount, - feesAmount, - currency, - statementDescriptor, - externalReference - ) - ) - } - - def loadDirectDebitTransaction(directDebitTransactionId: String): Future[TransactionResponse] = { - grpcClient.loadDirectDebitTransaction( - LoadDirectDebitTransactionRequest(directDebitTransactionId) - ) - } - - def registerRecurringPayment( - debitedAccount: String, - firstDebitedAmount: Int, - firstFeesAmount: Int, - currency: String, - `type`: RecurringPayment.RecurringPaymentType, - startDate: Option[Date], - endDate: Option[Date], - frequency: Option[RecurringPayment.RecurringPaymentFrequency], - fixedNextAmount: Option[Boolean], - nextDebitedAmount: Option[Int], - nextFeesAmount: Option[Int] - ): Future[Option[String]] = { - grpcClient.registerRecurringPayment( - RegisterRecurringPaymentRequest( - debitedAccount, - firstDebitedAmount, - firstFeesAmount, - currency, - `type`, - startDate, - endDate, - frequency match { - case Some(f) => f - case _ => - RegisterRecurringPaymentRequest.RecurringPaymentFrequency.UNKNOWN_PAYMENT_FREQUENCY - }, - fixedNextAmount, - nextDebitedAmount, - nextFeesAmount - ) - ) map (_.recurringPaymentRegistrationId) - } - - def cancelMandate(externalUuid: String): Future[Boolean] = { - grpcClient.cancelMandate(CancelMandateRequest(externalUuid)) map (_.succeeded) - } - - def loadBankAccountOwner(externalUuid: String): Future[BankAccountOwner] = { - grpcClient.loadBankAccountOwner(LoadBankAccountOwnerRequest(externalUuid)) map (response => - BankAccountOwner(response.ownerName, response.ownerAddress) - ) - } - - def loadLegalUserDetails(externalUuid: String): Future[LegalUserDetails] = { - grpcClient.loadLegalUser(LoadLegalUserRequest(externalUuid)) map (response => - LegalUserDetails( - response.legalUserType, - response.legalName, - response.siret, - response.legalRepresentativeAddress, - response.headQuartersAddress - ) - ) - } -} - -object PaymentClient extends GrpcClientFactory[PaymentClient] { - override val name: String = "PaymentService" - override def init(sys: ActorSystem[_]): PaymentClient = { - new PaymentClient { - override implicit lazy val system: ActorSystem[_] = sys - val name: String = PaymentClient.name - } - } -} diff --git a/common/src/main/scala/app/softnetwork/payment/config/PaymentSettings.scala b/common/src/main/scala/app/softnetwork/payment/config/PaymentSettings.scala index b601006..b7ad971 100644 --- a/common/src/main/scala/app/softnetwork/payment/config/PaymentSettings.scala +++ b/common/src/main/scala/app/softnetwork/payment/config/PaymentSettings.scala @@ -33,6 +33,7 @@ trait PaymentSettings extends StrictLogging { config.getString("payment.event-streams.external-to-payment-account-tag") val AkkaNodeRole: String = config.getString("payment.akka-node-role") + } object PaymentSettings extends PaymentSettings diff --git a/common/src/main/scala/app/softnetwork/payment/message/AccountMessages.scala b/common/src/main/scala/app/softnetwork/payment/message/AccountMessages.scala new file mode 100644 index 0000000..e7e877c --- /dev/null +++ b/common/src/main/scala/app/softnetwork/payment/message/AccountMessages.scala @@ -0,0 +1,77 @@ +package app.softnetwork.payment.message + +import app.softnetwork.account.message.{ + AccountCommand, + AccountCommandResult, + AccountErrorMessage, + LookupAccountCommand, + SignUp +} +import app.softnetwork.account.model.BasicAccountProfile +import app.softnetwork.payment.annotation.InternalApi +import app.softnetwork.payment.model.SoftPayAccount +import app.softnetwork.persistence.message.EntityCommand +import org.softnetwork.session.model.ApiKey + +object AccountMessages { + case class SoftPaySignUp( + login: String, + password: String, + provider: SoftPayAccount.Client.Provider, + override val confirmPassword: Option[String] = None, + override val profile: Option[BasicAccountProfile] = None + ) extends SignUp + + case class RegisterClientWithProvider(provider: SoftPayAccount.Client.Provider) + extends AccountCommand + + case class RegisterAccountWithProvider(provider: SoftPayAccount.Client.Provider) + extends AccountCommand + with EntityCommand { + override def id: String = provider.clientId + } + + @InternalApi + private[payment] case class LoadClient(clientId: String) extends LookupAccountCommand + + case class LoadApiKey(clientId: String) extends LookupAccountCommand + + case object ListApiKeys extends AccountCommand + + case class GenerateClientToken( + client_id: String, + client_secret: String, + scope: Option[String] = None + ) extends LookupAccountCommand + + case class RefreshClientToken(refreshToken: String) extends LookupAccountCommand + + case class OAuthClient(token: String) extends LookupAccountCommand + + case class ClientWithProviderRegistered(client: SoftPayAccount.Client) + extends AccountCommandResult + + case class AccountWithProviderRegistered(account: SoftPayAccount) extends AccountCommandResult + + case class ClientLoaded(client: SoftPayAccount.Client) extends AccountCommandResult + + case class ApiKeysLoaded(apiKeys: Seq[ApiKey]) extends AccountCommandResult + + case class ApiKeyLoaded(apiKey: ApiKey) extends AccountCommandResult + + case class OAuthClientSucceededResult(client: SoftPayAccount.Client) extends AccountCommandResult + + case object ProviderAlreadyRegistered extends AccountErrorMessage("provider.already.registered") + + case object ClientWithProviderNotRegistered + extends AccountErrorMessage("client.with.provider.not.registered") + + case object AccountWithProviderNotRegistered + extends AccountErrorMessage("account.with.provider.not.registered") + + case object ClientNotFound extends AccountErrorMessage("client.not.found") + + case object ApiKeyNotFound extends AccountErrorMessage("api.key.not.found") + + case class InactiveAccount(help: String) extends AccountErrorMessage(s"InactiveAccount\n$help") +} 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 ae2e1b4..971f447 100644 --- a/common/src/main/scala/app/softnetwork/payment/message/PaymentMessages.scala +++ b/common/src/main/scala/app/softnetwork/payment/message/PaymentMessages.scala @@ -27,9 +27,15 @@ object PaymentMessages { * - payment user * @param currency * - currency + * @param clientId + * - optional client id */ - case class PreRegisterCard(orderUuid: String, user: PaymentUser, currency: String = "EUR") - extends PaymentCommandWithKey { + case class PreRegisterCard( + orderUuid: String, + user: NaturalUser, + currency: String = "EUR", + clientId: Option[String] = None + ) extends PaymentCommandWithKey { val key: String = user.externalUuidWithProfile } @@ -140,9 +146,14 @@ object PaymentMessages { * - order unique id * @param cardPreAuthorizedTransactionId * - card pre authorized transaction id + * @param clientId + * - optional client id */ - case class CancelPreAuthorization(orderUuid: String, cardPreAuthorizedTransactionId: String) - extends PaymentCommandWithKey { + case class CancelPreAuthorization( + orderUuid: String, + cardPreAuthorizedTransactionId: String, + clientId: Option[String] = None + ) extends PaymentCommandWithKey { lazy val key: String = cardPreAuthorizedTransactionId } @@ -151,8 +162,10 @@ object PaymentMessages { * @param cardPreAuthorizedTransactionId * - card pre authorized transaction id */ - case class ValidatePreAuthorization(orderUuid: String, cardPreAuthorizedTransactionId: String) - extends PaymentCommandWithKey { + case class ValidatePreAuthorization( + orderUuid: String, + cardPreAuthorizedTransactionId: String + ) extends PaymentCommandWithKey { lazy val key: String = cardPreAuthorizedTransactionId } @@ -166,11 +179,14 @@ object PaymentMessages { * @param debitedAmount * - amount to be debited from the account that made the pre-authorization (if not specified it * will be the amount specified during the pre-authorization) + * @param clientId + * - optional client id */ case class PayInWithCardPreAuthorized( preAuthorizationId: String, creditedAccount: String, - debitedAmount: Option[Int] + debitedAmount: Option[Int], + clientId: Option[String] = None ) extends PaymentCommandWithKey { lazy val key: String = preAuthorizationId } @@ -259,24 +275,56 @@ object PaymentMessages { lazy val key: String = transactionId } + /** @param orderUuid + * - order uuid + * @param creditedAccount + * - account to credit + * @param creditedAmount + * - credited amount + * @param feesAmount + * - fees amount + * @param currency + * - currency + * @param externalReference + * - optional external reference + * @param clientId + * - optional client id + */ case class PayOut( orderUuid: String, creditedAccount: String, creditedAmount: Int, feesAmount: Int = 0, currency: String = "EUR", - externalReference: Option[String] = None + externalReference: Option[String] = None, + clientId: Option[String] = None ) extends PaymentCommandWithKey { val key: String = creditedAccount } + /** @param orderUuid + * - order uuid + * @param payInTransactionId + * - payIn transaction id + * @param refundAmount + * - refund amount + * @param currency + * - currency + * @param reasonMessage + * - reason message + * @param initializedByClient + * - whether the refund is initialized by the client or not + * @param clientId + * - optional client id + */ case class Refund( orderUuid: String, payInTransactionId: String, refundAmount: Int, currency: String = "EUR", reasonMessage: String, - initializedByClient: Boolean + initializedByClient: Boolean, + clientId: Option[String] = None ) extends PaymentCommandWithKey { lazy val key: String = payInTransactionId } @@ -297,6 +345,8 @@ object PaymentMessages { * - whether an immediate pay out is required or not * @param externalReference * - optional external reference + * @param clientId + * - optional client id */ case class Transfer( orderUuid: Option[String] = None, @@ -306,7 +356,8 @@ object PaymentMessages { feesAmount: Int = 0, currency: String = "EUR", payOutRequired: Boolean = true, - externalReference: Option[String] = None + externalReference: Option[String] = None, + clientId: Option[String] = None ) extends PaymentCommandWithKey { val key: String = debitedAccount } @@ -323,6 +374,8 @@ object PaymentMessages { * - statement descriptor * @param externalReference * - optional external reference + * @param clientId + * - optional client id */ case class DirectDebit( creditedAccount: String, @@ -330,16 +383,21 @@ object PaymentMessages { feesAmount: Int = 0, currency: String = "EUR", statementDescriptor: String, - externalReference: Option[String] = None + externalReference: Option[String] = None, + clientId: Option[String] = None ) extends PaymentCommandWithKey { val key: String = creditedAccount } /** @param directDebitTransactionId * - direct debit transaction id + * @param clientId + * - optional client id */ - case class LoadDirectDebitTransaction(directDebitTransactionId: String) - extends PaymentCommandWithKey { + case class LoadDirectDebitTransaction( + directDebitTransactionId: String, + clientId: Option[String] = None + ) extends PaymentCommandWithKey { val key: String = directDebitTransactionId } @@ -365,6 +423,12 @@ object PaymentMessages { * - next debited amount * @param nextFeesAmount * - next fees amount + * @param statementDescriptor + * - statement descriptor + * @param externalReference + * - optional external reference + * @param clientId + * - optional client id */ case class RegisterRecurringPayment( debitedAccount: String, @@ -377,7 +441,10 @@ object PaymentMessages { frequency: Option[RecurringPayment.RecurringPaymentFrequency] = None, fixedNextAmount: Option[Boolean] = None, nextDebitedAmount: Option[Int] = None, - nextFeesAmount: Option[Int] = None + nextFeesAmount: Option[Int] = None, + statementDescriptor: Option[String] = None, + externalReference: Option[String] = None, + clientId: Option[String] = None ) extends PaymentCommandWithKey { val key: String = debitedAccount } @@ -448,6 +515,8 @@ object PaymentMessages { * - next debited amount * @param nextFeesAmount * - next fees amount + * @param statementDescriptor + * - statement descriptor */ case class PayNextRecurring( recurringPaymentRegistrationId: String, @@ -469,12 +538,18 @@ object PaymentMessages { * * @param account * - payment account reference + * @param clientId + * - optional client id */ @InternalApi - private[payment] case class LoadPaymentAccount(account: String) extends PaymentCommandWithKey { + private[payment] case class LoadPaymentAccount(account: String, clientId: Option[String] = None) + extends PaymentCommandWithKey { lazy val key: String = account } + /** @param transactionId + * - transaction id + */ case class LoadTransaction(transactionId: String) extends PaymentCommandWithKey { lazy val key: String = transactionId } @@ -483,7 +558,7 @@ object PaymentMessages { case class BankAccountCommand( bankAccount: BankAccount, - user: Either[PaymentUser, LegalUser], + user: Either[NaturalUser, LegalUser], acceptedTermsOfPSP: Option[Boolean] = None ) @@ -491,7 +566,7 @@ object PaymentMessages { def apply( bankAccount: BankAccount, - naturalUser: PaymentUser, + naturalUser: NaturalUser, acceptedTermsOfPSP: Option[Boolean] ): BankAccountCommand = BankAccountCommand(bankAccount, Left(naturalUser), acceptedTermsOfPSP) @@ -502,34 +577,73 @@ object PaymentMessages { ): BankAccountCommand = BankAccountCommand(bankAccount, Right(legalUser), acceptedTermsOfPSP) } + /** @param creditedAccount + * - account to credit + * @param bankAccount + * - bank account + * @param user + * - payment user + * @param acceptedTermsOfPSP + * - whether or not the terms of the psp are accepted + * @param clientId + * - optional client id + */ case class CreateOrUpdateBankAccount( creditedAccount: String, bankAccount: BankAccount, user: Option[PaymentAccount.User] = None, - acceptedTermsOfPSP: Option[Boolean] = None + acceptedTermsOfPSP: Option[Boolean] = None, + clientId: Option[String] = None ) extends PaymentCommandWithKey { val key: String = creditedAccount } - case class LoadBankAccount(creditedAccount: String) extends PaymentCommandWithKey { + /** @param creditedAccount + * - account to load + * @param clientId + * - optional client id + */ + case class LoadBankAccount(creditedAccount: String, clientId: Option[String] = None) + extends PaymentCommandWithKey { val key: String = creditedAccount } - case class DeleteBankAccount(creditedAccount: String, force: Option[Boolean]) - extends PaymentCommandWithKey { + /** @param creditedAccount + * - account to delete + * @param force + * - whether or not the bank account should be deleted even if the bank account deletion has + * been disabled + */ + case class DeleteBankAccount( + creditedAccount: String, + force: Option[Boolean] + ) extends PaymentCommandWithKey { val key: String = creditedAccount } + /** @param debitedAccount + * - account owning the cards to load + */ case class LoadCards(debitedAccount: String) extends PaymentCommandWithKey { val key: String = debitedAccount } + /** @param debitedAccount + * - account owning the card to disable + * @param cardId + * - card id + */ case class DisableCard(debitedAccount: String, cardId: String) extends PaymentCommandWithKey { val key: String = debitedAccount } /** Commands related to the kyc documents */ + /** @param creditedAccount + * - account to whom the KYC document would be added + * @param pages + * - pages of the KYC document + */ case class AddKycDocument( creditedAccount: String, pages: Seq[Array[Byte]], @@ -553,6 +667,11 @@ object PaymentMessages { lazy val key: String = kycDocumentId } + /** @param creditedAccount + * - account which owns the KYC document that would be loaded + * @param kycDocumentType + * - KYC document type + */ case class LoadKycDocumentStatus( creditedAccount: String, kycDocumentType: KycDocument.KycDocumentType @@ -562,15 +681,28 @@ object PaymentMessages { /** Commands related to the ubo declaration */ - case class CreateOrUpdateUbo(creditedAccount: String, ubo: UboDeclaration.UltimateBeneficialOwner) - extends PaymentCommandWithKey { + /** @param creditedAccount + * - account to whom the UBO declaration would be added + * @param ubo + * - ultimate beneficial owner + */ + case class CreateOrUpdateUbo( + creditedAccount: String, + ubo: UboDeclaration.UltimateBeneficialOwner + ) extends PaymentCommandWithKey { val key: String = creditedAccount } + /** @param creditedAccount + * - account which owns the UBO declaration that would be validated + */ case class ValidateUboDeclaration(creditedAccount: String) extends PaymentCommandWithKey { val key: String = creditedAccount } + /** @param creditedAccount + * - account which owns the UBO declaration that would be loaded + */ case class GetUboDeclaration(creditedAccount: String) extends PaymentCommandWithKey { val key: String = creditedAccount } @@ -592,11 +724,23 @@ object PaymentMessages { /** Commands related to the mandate */ - case class CreateMandate(creditedAccount: String) extends PaymentCommandWithKey { + /** @param creditedAccount + * - account to whom the mandate would be added + * @param clientId + * - optional client id + */ + case class CreateMandate(creditedAccount: String, clientId: Option[String] = None) + extends PaymentCommandWithKey { val key: String = creditedAccount } - case class CancelMandate(creditedAccount: String) extends PaymentCommandWithKey { + /** @param creditedAccount + * - account which owns the mandate that would be canceled + * @param clientId + * - optional client id + */ + case class CancelMandate(creditedAccount: String, clientId: Option[String] = None) + extends PaymentCommandWithKey { val key: String = creditedAccount } @@ -635,6 +779,9 @@ object PaymentMessages { lazy val key: String = userId } + /** @param paymentAccount + * - payment account + */ @InternalApi private[payment] case class CreateOrUpdatePaymentAccount(paymentAccount: PaymentAccount) extends PaymentCommandWithKey { diff --git a/common/src/main/scala/app/softnetwork/payment/model/SoftPayAccountCompanion.scala b/common/src/main/scala/app/softnetwork/payment/model/SoftPayAccountCompanion.scala new file mode 100644 index 0000000..c33f4b6 --- /dev/null +++ b/common/src/main/scala/app/softnetwork/payment/model/SoftPayAccountCompanion.scala @@ -0,0 +1,15 @@ +package app.softnetwork.payment.model + +import app.softnetwork.account.model.{BasicAccount, BasicAccountCompanion} +import app.softnetwork.payment.serialization._ + +trait SoftPayAccountCompanion extends BasicAccountCompanion { + + def apply(account: Option[BasicAccount]): Option[SoftPayAccount] = { + account match { + case Some(a) => Some(a) + case _ => None + } + } + +} diff --git a/common/src/main/scala/app/softnetwork/payment/model/SoftPayAccountDecorator.scala b/common/src/main/scala/app/softnetwork/payment/model/SoftPayAccountDecorator.scala new file mode 100644 index 0000000..0a3da8c --- /dev/null +++ b/common/src/main/scala/app/softnetwork/payment/model/SoftPayAccountDecorator.scala @@ -0,0 +1,13 @@ +package app.softnetwork.payment.model + +import app.softnetwork.account.model.{BasicAccountProfile, Profile} +import org.softnetwork.session.model.ApiKey + +trait SoftPayAccountDecorator { _: SoftPayAccount => + + override def newProfile(name: String): Profile = + BasicAccountProfile.defaultInstance.withName(name) + + lazy val apiKeys: Seq[ApiKey] = + this.clients.map(client => ApiKey(client.clientId, client.clientApiKey)) +} diff --git a/common/src/main/scala/app/softnetwork/payment/model/SoftPayClientDecorator.scala b/common/src/main/scala/app/softnetwork/payment/model/SoftPayClientDecorator.scala new file mode 100644 index 0000000..8ba4f59 --- /dev/null +++ b/common/src/main/scala/app/softnetwork/payment/model/SoftPayClientDecorator.scala @@ -0,0 +1,45 @@ +package app.softnetwork.payment.model + +import app.softnetwork.account.model.BearerTokenGenerator + +trait SoftPayClientDecorator { _: SoftPayAccount.Client => + + def generateApiKey(): String = BearerTokenGenerator.generateSHAToken(clientId) + + lazy val view: SoftPayClientView = SoftPayClientView(this) +} + +case class SoftPayClientView( + clientId: String, + clientApiKey: Option[String] = None, + name: Option[String] = None, + description: Option[String] = None, + websiteUrl: Option[String] = None, + logoUrl: Option[String] = None, + technicalEmails: Seq[String] = Seq.empty, + administrativeEmails: Seq[String] = Seq.empty, + billingEmails: Seq[String] = Seq.empty, + fraudEmails: Seq[String] = Seq.empty, + vatNumber: Option[String] = None, + address: Option[AddressView] = None +) + +object SoftPayClientView { + def apply(client: SoftPayAccount.Client): SoftPayClientView = { + import client._ + SoftPayClientView( + clientId, + clientApiKey, + name, + description, + websiteUrl, + logoUrl, + technicalEmails, + administrativeEmails, + billingEmails, + fraudEmails, + vatNumber, + address.map(AddressView(_)) + ) + } +} diff --git a/common/src/main/scala/app/softnetwork/payment/model/SoftPayProviderDecorator.scala b/common/src/main/scala/app/softnetwork/payment/model/SoftPayProviderDecorator.scala new file mode 100644 index 0000000..1678001 --- /dev/null +++ b/common/src/main/scala/app/softnetwork/payment/model/SoftPayProviderDecorator.scala @@ -0,0 +1,26 @@ +package app.softnetwork.payment.model + +import app.softnetwork.account.model.AccountStatus +import app.softnetwork.payment.spi.PaymentProviders +import app.softnetwork.security.sha256 + +trait SoftPayProviderDecorator { self: SoftPayAccount.Client.Provider => + lazy val clientId = s"$providerId.${providerType.name.toLowerCase}" + + lazy val client: SoftPayAccount.Client = { + PaymentProviders.paymentProvider(self).client match { + case Some(client) => + client.withClientApiKey(sha256(self.providerApiKey)) + case _ => + throw new Exception(s"PaymentProvider not found for providerType: $providerType") + } + } + + lazy val account: SoftPayAccount = { + SoftPayAccount.defaultInstance + .withUuid(clientId) + .withAnonymous(true) + .withClients(Seq(client)) + .withStatus(AccountStatus.Active) + } +} diff --git a/common/src/main/scala/app/softnetwork/payment/serialization/package.scala b/common/src/main/scala/app/softnetwork/payment/serialization/package.scala index 0625bb0..22fc43b 100644 --- a/common/src/main/scala/app/softnetwork/payment/serialization/package.scala +++ b/common/src/main/scala/app/softnetwork/payment/serialization/package.scala @@ -3,9 +3,10 @@ package app.softnetwork.payment import app.softnetwork.payment.model._ import app.softnetwork.protobuf.ScalaPBSerializers import ScalaPBSerializers.GeneratedEnumSerializer -import app.softnetwork.payment.api.{LegalUserType, TransactionStatus} +import app.softnetwork.account.model.Account +import app.softnetwork.account.serialization.accountFormats +import app.softnetwork.payment.api.TransactionStatus import org.json4s.Formats -import app.softnetwork.serialization._ import scala.language.implicitConversions @@ -13,13 +14,13 @@ import scala.language.implicitConversions */ package object serialization { - val paymentFormats: Formats = commonFormats ++ + val paymentFormats: Formats = accountFormats ++ Seq( GeneratedEnumSerializer(KycDocument.KycDocumentStatus.enumCompanion), GeneratedEnumSerializer(KycDocument.KycDocumentType.enumCompanion), GeneratedEnumSerializer(UboDeclaration.UboDeclarationStatus.enumCompanion), GeneratedEnumSerializer(Transaction.PaymentType.enumCompanion), - GeneratedEnumSerializer(PaymentUser.PaymentUserType.enumCompanion), + GeneratedEnumSerializer(NaturalUser.NaturalUserType.enumCompanion), GeneratedEnumSerializer(LegalUser.LegalUserType.enumCompanion), GeneratedEnumSerializer(PaymentAccount.PaymentAccountStatus.enumCompanion), GeneratedEnumSerializer(BankAccount.MandateStatus.enumCompanion), @@ -28,7 +29,10 @@ package object serialization { GeneratedEnumSerializer(Transaction.TransactionType.enumCompanion), GeneratedEnumSerializer(RecurringPayment.RecurringPaymentType.enumCompanion), GeneratedEnumSerializer(RecurringPayment.RecurringPaymentFrequency.enumCompanion), - GeneratedEnumSerializer(RecurringPayment.RecurringCardPaymentStatus.enumCompanion) + GeneratedEnumSerializer(RecurringPayment.RecurringCardPaymentStatus.enumCompanion), + GeneratedEnumSerializer( + SoftPayAccount.Client.Provider.ProviderType.enumCompanion + ) ) implicit def transactionStatusToTransactionResponseStatus( @@ -65,23 +69,32 @@ package object serialization { } } - implicit def LegalUserTypeToLegalResponseUserType( - legalUserType: LegalUser.LegalUserType - ): LegalUserType = { - legalUserType match { - case LegalUser.LegalUserType.BUSINESS => LegalUserType.BUSINESS - case LegalUser.LegalUserType.SOLETRADER => LegalUserType.SOLETRADER - case LegalUser.LegalUserType.ORGANIZATION => LegalUserType.ORGANIZATION - } - } - - implicit def LegalResponseUserTypeToLegalUserType( - legalUserType: LegalUserType - ): LegalUser.LegalUserType = { - legalUserType match { - case LegalUserType.BUSINESS => LegalUser.LegalUserType.BUSINESS - case LegalUserType.SOLETRADER => LegalUser.LegalUserType.SOLETRADER - case LegalUserType.ORGANIZATION => LegalUser.LegalUserType.ORGANIZATION + implicit def accountToSoftPaymentAccount(account: Account): SoftPayAccount = { + account match { + case a: SoftPayAccount => a + case _ => + import account._ + SoftPayAccount.defaultInstance + .withApplications(applications) + .withCreatedDate(createdDate) + .withCredentials(credentials) + .withCurrentProfile(currentProfile.orNull) + .withDetails(details.orNull) + .withLastLogin(lastLogin.orNull) + .withLastUpdated(lastUpdated) + .withNbLoginFailures(nbLoginFailures) + .withPrincipal(principal) + .withProfiles(profiles) + .withRegistrations(registrations) + .withSecondaryPrincipals(secondaryPrincipals) + .withStatus(status) + .withUuid(uuid) + .withVerificationCode(verificationCode.orNull) + .withVerificationToken(verificationToken.orNull) + .copy( + anonymous = anonymous, + fromAnonymous = fromAnonymous + ) } } } diff --git a/common/src/main/scala/app/softnetwork/payment/spi/PaymentProvider.scala b/common/src/main/scala/app/softnetwork/payment/spi/PaymentProvider.scala index 6904a57..8c1bf95 100644 --- a/common/src/main/scala/app/softnetwork/payment/spi/PaymentProvider.scala +++ b/common/src/main/scala/app/softnetwork/payment/spi/PaymentProvider.scala @@ -12,6 +12,8 @@ private[payment] trait PaymentProvider { protected lazy val mlog: Logger = Logger(LoggerFactory.getLogger(getClass.getName)) + implicit def provider: SoftPayAccount.Client.Provider + /** @param maybePaymentAccount * - payment account to create or update * @return @@ -37,7 +39,7 @@ private[payment] trait PaymentProvider { * @return * provider user id */ - def createOrUpdateNaturalUser(maybeNaturalUser: Option[PaymentUser]): Option[String] + def createOrUpdateNaturalUser(maybeNaturalUser: Option[NaturalUser]): Option[String] /** @param maybeLegalUser * - legal user to create @@ -372,6 +374,8 @@ private[payment] trait PaymentProvider { /** @return * client fees */ + def client: Option[SoftPayAccount.Client] + def clientFees(): Option[Double] /** @param userId diff --git a/common/src/main/scala/app/softnetwork/payment/spi/PaymentProviderSpi.scala b/common/src/main/scala/app/softnetwork/payment/spi/PaymentProviderSpi.scala new file mode 100644 index 0000000..6ff737c --- /dev/null +++ b/common/src/main/scala/app/softnetwork/payment/spi/PaymentProviderSpi.scala @@ -0,0 +1,11 @@ +package app.softnetwork.payment.spi + +import app.softnetwork.payment.model.SoftPayAccount + +trait PaymentProviderSpi { + def providerType: SoftPayAccount.Client.Provider.ProviderType + + def paymentProvider(p: SoftPayAccount.Client.Provider): PaymentProvider + + def softPaymentProvider: SoftPayAccount.Client.Provider +} diff --git a/common/src/main/scala/app/softnetwork/payment/spi/PaymentProviders.scala b/common/src/main/scala/app/softnetwork/payment/spi/PaymentProviders.scala new file mode 100644 index 0000000..cb41a6f --- /dev/null +++ b/common/src/main/scala/app/softnetwork/payment/spi/PaymentProviders.scala @@ -0,0 +1,40 @@ +package app.softnetwork.payment.spi + +import app.softnetwork.payment.model.SoftPayAccount + +import java.util.ServiceLoader +import scala.collection.JavaConverters._ + +object PaymentProviders { + + private[this] lazy val paymentProviderFactories: ServiceLoader[PaymentProviderSpi] = + ServiceLoader.load(classOf[PaymentProviderSpi]) + + private[this] var paymentProviders: Map[String, PaymentProvider] = Map.empty + + def defaultPaymentProviders: Seq[SoftPayAccount.Client.Provider] = + paymentProviderFactories.iterator().asScala.map(_.softPaymentProvider).toSeq + + def paymentProvider( + provider: SoftPayAccount.Client.Provider + ): PaymentProvider = { + paymentProviders.get(provider.clientId) match { + case Some(paymentProvider) => paymentProvider + case _ => + val paymentProvider = + paymentProviderFactories + .iterator() + .asScala + .find(_.providerType == provider.providerType) + .map(_.paymentProvider(provider)) + .getOrElse( + throw new Exception( + s"PaymentProvider not found for providerType: ${provider.providerType}" + ) + ) + paymentProviders += provider.clientId -> paymentProvider + paymentProvider + } + } + +} diff --git a/core/build.sbt b/core/build.sbt index 4b01b80..8877f62 100644 --- a/core/build.sbt +++ b/core/build.sbt @@ -2,7 +2,12 @@ organization := "app.softnetwork.payment" name := "payment-core" +akkaGrpcGeneratedSources := Seq(AkkaGrpc.Server) + +Compile / PB.protoSources := Seq(sourceDirectory.value / ".." / ".." / "client/src/main/protobuf/api") + libraryDependencies ++= Seq( "app.softnetwork.persistence" %% "persistence-kv" % Versions.genericPersistence, + "app.softnetwork.account" %% "account-core" % Versions.account, "app.softnetwork.session" %% "session-core" % Versions.genericPersistence ) diff --git a/core/src/main/resources/snippets/account/inactive.mustache b/core/src/main/resources/snippets/account/inactive.mustache new file mode 100644 index 0000000..91bc4ab --- /dev/null +++ b/core/src/main/resources/snippets/account/inactive.mustache @@ -0,0 +1,7 @@ +Please activate your account ! + +Using the cli : +{{command}} + +Using the API : +curl -X GET {{activationUrl}} \ No newline at end of file diff --git a/core/src/main/scala/app/softnetwork/payment/api/ClientGrpcService.scala b/core/src/main/scala/app/softnetwork/payment/api/ClientGrpcService.scala new file mode 100644 index 0000000..24b2f2d --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/api/ClientGrpcService.scala @@ -0,0 +1,14 @@ +package app.softnetwork.payment.api + +import akka.actor.typed.ActorSystem +import akka.http.scaladsl.model.{HttpRequest, HttpResponse} +import app.softnetwork.api.server.GrpcService + +import scala.concurrent.Future + +class ClientGrpcService(server: ClientServer) extends GrpcService { + + override def grpcService: ActorSystem[_] => PartialFunction[HttpRequest, Future[HttpResponse]] = + system => ClientServiceApiHandler.partial(server)(system) + +} diff --git a/core/src/main/scala/app/softnetwork/payment/api/ClientServer.scala b/core/src/main/scala/app/softnetwork/payment/api/ClientServer.scala new file mode 100644 index 0000000..b574112 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/api/ClientServer.scala @@ -0,0 +1,101 @@ +package app.softnetwork.payment.api + +import akka.actor.typed.ActorSystem +import app.softnetwork.account.message.Activate +import app.softnetwork.payment.handlers.SoftPayAccountDao +import app.softnetwork.payment.message.AccountMessages +import app.softnetwork.payment.model.SoftPayAccount +import org.slf4j.{Logger, LoggerFactory} + +import scala.concurrent.{ExecutionContextExecutor, Future} + +trait ClientServer extends ClientServiceApi with SoftPayAccountDao { + + implicit def system: ActorSystem[_] + + implicit lazy val ec: ExecutionContextExecutor = system.executionContext + + override def generateClientTokens( + in: GenerateClientTokensRequest + ): Future[ClientTokensResponse] = { + import in._ + generateClientTokens(clientId, clientSecret, scope) map { + case Right(tokens) => + import tokens._ + ClientTokensResponse.defaultInstance.withTokens( + Tokens( + access_token, + token_type, + expires_in, + refresh_token, + refresh_token_expires_in.map(_.toLong) + ) + ) + case Left(error) => ClientTokensResponse.defaultInstance.withError(error) + } + } + + override def refreshClientTokens(in: RefreshClientTokensRequest): Future[ClientTokensResponse] = { + import in._ + refreshClientTokens(refreshToken) map { + case Right(tokens) => + import tokens._ + ClientTokensResponse.defaultInstance.withTokens( + Tokens( + access_token, + token_type, + expires_in, + refresh_token, + refresh_token_expires_in.map(_.toLong) + ) + ) + case Left(error) => ClientTokensResponse.defaultInstance.withError(error) + } + } + + override def signUpClient(in: SignUpClientRequest): Future[SignUpClientResponse] = { + import in._ + signUpClient( + AccountMessages.SoftPaySignUp( + principal, + credentials, + SoftPayAccount.Client.Provider( + providerId, + providerApiKey, + SoftPayAccount.Client.Provider.ProviderType + .fromName(providerType.name) + .getOrElse(SoftPayAccount.Client.Provider.ProviderType.MANGOPAY) + ) + ) + ) map { + case Right(client) => + import client._ + SignUpClientResponse.defaultInstance.withClient( + ClientCreated( + clientId, + clientApiKey.getOrElse("") + ) + ) + case Left(error) => SignUpClientResponse.defaultInstance.withError(error) + } + } + + override def activateClient(in: ActivateClientRequest): Future[ActivateClientResponse] = { + import in._ + activateClient(Activate(token)) map { + case Right(activated) => ActivateClientResponse.defaultInstance.withActivated(activated) + case Left(error) => + ActivateClientResponse.defaultInstance.withError(error) + } + } +} + +object ClientServer { + + def apply(sys: ActorSystem[_]): ClientServer = { + new ClientServer { + lazy val log: Logger = LoggerFactory getLogger getClass.getName + override implicit val system: ActorSystem[_] = sys + } + } +} diff --git a/core/src/main/scala/app/softnetwork/payment/api/PaymentGrpcService.scala b/core/src/main/scala/app/softnetwork/payment/api/PaymentGrpcService.scala new file mode 100644 index 0000000..e5ab3b0 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/api/PaymentGrpcService.scala @@ -0,0 +1,49 @@ +package app.softnetwork.payment.api + +import akka.actor.typed.ActorSystem +import akka.http.scaladsl.model.headers.HttpChallenges +import akka.http.scaladsl.model.{HttpRequest, HttpResponse} +import akka.http.scaladsl.server.{AuthenticationFailedRejection, Route} +import akka.http.scaladsl.server.directives.Credentials +import app.softnetwork.account.config.AccountSettings +import app.softnetwork.api.server.GrpcService +import app.softnetwork.concurrent.Completion +import app.softnetwork.payment.handlers.SoftPayAccountDao + +import scala.concurrent.Future + +class PaymentGrpcService(server: PaymentServer, softPayAccountDao: SoftPayAccountDao) + extends GrpcService + with Completion { + override def grpcService: ActorSystem[_] => PartialFunction[HttpRequest, Future[HttpResponse]] = + system => PaymentServiceApiHandler.partial(server)(system) + + override def route: ActorSystem[_] => Route = system => + authenticateOAuth2Async( + AccountSettings.Realm, + { + case _ @Credentials.Provided(token) => + softPayAccountDao.authenticateClient(Some(token))(system) + case _ => Future.successful(None) + } + ).optional { + case (Some(client)) => + handle( + grpcService(system), + Seq( + AuthenticationFailedRejection( + AuthenticationFailedRejection.CredentialsRejected, + HttpChallenges.oAuth2(AccountSettings.Realm) + ) + ) + ) + case _ => + reject( + AuthenticationFailedRejection( + AuthenticationFailedRejection.CredentialsRejected, + HttpChallenges.oAuth2(AccountSettings.Realm) + ) + ) + } + +} diff --git a/core/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServices.scala b/core/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServices.scala new file mode 100644 index 0000000..7f43d42 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServices.scala @@ -0,0 +1,10 @@ +package app.softnetwork.payment.api + +import akka.actor.typed.ActorSystem +import app.softnetwork.api.server.{GrpcService, GrpcServices} +import app.softnetwork.payment.launch.PaymentGuardian + +trait PaymentGrpcServices extends GrpcServices { _: PaymentGuardian => + override def grpcServices: ActorSystem[_] => Seq[GrpcService] = system => + paymentGrpcServices(system) +} diff --git a/core/src/main/scala/app/softnetwork/payment/api/PaymentServer.scala b/core/src/main/scala/app/softnetwork/payment/api/PaymentServer.scala index 578d98c..1f3443d 100644 --- a/core/src/main/scala/app/softnetwork/payment/api/PaymentServer.scala +++ b/core/src/main/scala/app/softnetwork/payment/api/PaymentServer.scala @@ -1,49 +1,32 @@ package app.softnetwork.payment.api import akka.actor.typed.ActorSystem -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.api.serialization._ +import app.softnetwork.payment.handlers.PaymentDao import app.softnetwork.payment.message.PaymentMessages.{ - BankAccountLoaded, - CancelMandate, - CancelPreAuthorization, CreateOrUpdatePaymentAccount, - DirectDebit, DirectDebitFailed, DirectDebited, - LoadBankAccount, - LoadDirectDebitTransaction, - LoadPaymentAccount, - MandateCanceled, PaidIn, PaidOut, - PayInFailed, - PayInWithCardPreAuthorized, - PayOut, PayOutFailed, PaymentAccountCreated, - PaymentAccountLoaded, PaymentAccountUpdated, - PaymentCommand, - PaymentError, PreAuthorizationCanceled, RecurringPaymentRegistered, - Refund, RefundFailed, Refunded, - RegisterRecurringPayment, - Transfer, TransferFailed, Transferred } -import app.softnetwork.payment.model.RecurringPayment +import app.softnetwork.payment.model.{BankAccount, PaymentAccount, RecurringPayment} import app.softnetwork.payment.serialization._ -import app.softnetwork.persistence.typed.CommandTypeKey +import org.slf4j.{Logger, LoggerFactory} import scala.concurrent.{ExecutionContextExecutor, Future} import scala.language.implicitConversions -trait PaymentServer extends PaymentServiceApi with GenericPaymentHandler { - _: CommandTypeKey[PaymentCommand] => +trait PaymentServer extends PaymentServiceApi with PaymentDao { implicit def system: ActorSystem[_] implicit lazy val ec: ExecutionContextExecutor = system.executionContext @@ -66,28 +49,23 @@ trait PaymentServer extends PaymentServiceApi with GenericPaymentHandler { in: PayInWithCardPreAuthorizedRequest ): Future[TransactionResponse] = { import in._ - !?(PayInWithCardPreAuthorized(preAuthorizationId, creditedAccount, debitedAmount)) map { - case r: PaidIn => + payInWithCardPreAuthorized( + preAuthorizationId, + creditedAccount, + debitedAmount, + Some(clientId) + ) map { + case Right(r: PaidIn) => TransactionResponse( transactionId = Some(r.transactionId), transactionStatus = r.transactionStatus ) - case f: PayInFailed => + case Left(f) => TransactionResponse( - transactionId = Some(f.transactionId), + transactionId = if (f.transactionId.isEmpty) None else Some(f.transactionId), transactionStatus = f.transactionStatus, error = Some(f.message) ) - case e: PaymentError => - TransactionResponse( - transactionStatus = TransactionStatus.TRANSACTION_NOT_SPECIFIED, - error = Some(e.message) - ) - case _ => - TransactionResponse( - transactionStatus = TransactionStatus.TRANSACTION_NOT_SPECIFIED, - error = Some("unknown") - ) } } @@ -95,8 +73,8 @@ trait PaymentServer extends PaymentServiceApi with GenericPaymentHandler { in: CancelPreAuthorizationRequest ): Future[CancelPreAuthorizationResponse] = { import in._ - !?(CancelPreAuthorization(orderUuid, cardPreAuthorizedTransactionId)) map { - case r: PreAuthorizationCanceled => + cancelPreAuthorization(orderUuid, cardPreAuthorizedTransactionId, Some(clientId)) map { + case Right(r: PreAuthorizationCanceled) => CancelPreAuthorizationResponse(Some(r.preAuthorizationCanceled)) case _ => CancelPreAuthorizationResponse() } @@ -104,141 +82,104 @@ trait PaymentServer extends PaymentServiceApi with GenericPaymentHandler { override def refund(in: RefundRequest): Future[TransactionResponse] = { import in._ - !?( - Refund( - orderUuid, - payInTransactionId, - refundAmount, - currency, - reasonMessage, - initializedByClient - ) + refund( + orderUuid, + payInTransactionId, + refundAmount, + currency, + reasonMessage, + initializedByClient, + Some(clientId) ) map { - case r: Refunded => + case Right(r: Refunded) => TransactionResponse( transactionId = Some(r.transactionId), transactionStatus = r.transactionStatus ) - case f: RefundFailed => + case Left(f: RefundFailed) => TransactionResponse( - transactionId = Some(f.transactionId), + transactionId = if (f.transactionId.isEmpty) None else Some(f.transactionId), transactionStatus = f.transactionStatus, error = Some(f.message) ) - case e: PaymentError => - TransactionResponse( - transactionStatus = TransactionStatus.TRANSACTION_NOT_SPECIFIED, - error = Some(e.message) - ) - case _ => - TransactionResponse( - transactionStatus = TransactionStatus.TRANSACTION_NOT_SPECIFIED, - error = Some("unknown") - ) } } override def payOut(in: PayOutRequest): Future[TransactionResponse] = { import in._ - !?( - PayOut(orderUuid, creditedAccount, creditedAmount, feesAmount, currency, externalReference) + payOut( + orderUuid, + creditedAccount, + creditedAmount, + feesAmount, + currency, + externalReference, + Some(clientId) ) map { - case r: PaidOut => + case Right(r: PaidOut) => TransactionResponse( transactionId = Some(r.transactionId), transactionStatus = r.transactionStatus ) - case f: PayOutFailed => + case Left(f: PayOutFailed) => TransactionResponse( - transactionId = Some(f.transactionId), + transactionId = if (f.transactionId.isEmpty) None else Some(f.transactionId), transactionStatus = f.transactionStatus, error = Some(f.message) ) - case e: PaymentError => - TransactionResponse( - transactionStatus = TransactionStatus.TRANSACTION_NOT_SPECIFIED, - error = Some(e.message) - ) - case _ => - TransactionResponse( - transactionStatus = TransactionStatus.TRANSACTION_NOT_SPECIFIED, - error = Some("unknown") - ) } } override def transfer(in: TransferRequest): Future[TransferResponse] = { import in._ - !?( - Transfer( - orderUuid, - debitedAccount, - creditedAccount, - debitedAmount, - feesAmount, - currency, - payOutRequired, - externalReference - ) + transfer( + orderUuid, + debitedAccount, + creditedAccount, + debitedAmount, + feesAmount, + currency, + payOutRequired, + externalReference, + Some(clientId) ) map { - case r: Transferred => + case Right(r: Transferred) => TransferResponse( Some(r.transferredTransactionId), r.transferredTransactionStatus, r.paidOutTransactionId ) - case f: TransferFailed => + case Left(f: TransferFailed) => TransferResponse( - Some(f.transferredTransactionId), + if (f.transferredTransactionId.isEmpty) None else Some(f.transferredTransactionId), f.transferredTransactionStatus, error = Some(f.message) ) - case e: PaymentError => - TransferResponse( - transferredTransactionStatus = TransactionStatus.TRANSACTION_NOT_SPECIFIED, - error = Some(e.message) - ) - case _ => - TransferResponse( - transferredTransactionStatus = TransactionStatus.TRANSACTION_NOT_SPECIFIED, - error = Some("unknown") - ) } } override def directDebit(in: DirectDebitRequest): Future[TransactionResponse] = { import in._ - !?( - DirectDebit( - creditedAccount, - debitedAmount, - feesAmount, - currency, - statementDescriptor, - externalReference - ) + directDebit( + creditedAccount, + debitedAmount, + feesAmount, + currency, + statementDescriptor, + externalReference, + Some(clientId) ) map { - case r: DirectDebited => + case Right(r: DirectDebited) => TransactionResponse( transactionId = Some(r.transactionId), transactionStatus = r.transactionStatus ) - case f: DirectDebitFailed => + case Left(f: DirectDebitFailed) => TransactionResponse( - transactionId = Some(f.transactionId), + transactionId = if (f.transactionId.isEmpty) None else Some(f.transactionId), transactionStatus = f.transactionStatus, error = Some(f.message) ) - case e: PaymentError => - TransactionResponse( - transactionStatus = TransactionStatus.TRANSACTION_NOT_SPECIFIED, - error = Some(e.message) - ) - case _ => - TransactionResponse( - transactionStatus = TransactionStatus.TRANSACTION_NOT_SPECIFIED, - error = Some("unknown") - ) } } @@ -246,28 +187,18 @@ trait PaymentServer extends PaymentServiceApi with GenericPaymentHandler { in: LoadDirectDebitTransactionRequest ): Future[TransactionResponse] = { import in._ - !?(LoadDirectDebitTransaction(directDebitTransactionId)) map { - case r: DirectDebited => + loadDirectDebitTransaction(directDebitTransactionId, Some(clientId)) map { + case Right(r: DirectDebited) => TransactionResponse( transactionId = Some(r.transactionId), transactionStatus = r.transactionStatus ) - case f: DirectDebitFailed => + case Left(f: DirectDebitFailed) => TransactionResponse( - transactionId = Some(f.transactionId), + transactionId = if (f.transactionId.isEmpty) None else Some(f.transactionId), transactionStatus = f.transactionStatus, error = Some(f.message) ) - case e: PaymentError => - TransactionResponse( - transactionStatus = TransactionStatus.TRANSACTION_NOT_SPECIFIED, - error = Some(e.message) - ) - case _ => - TransactionResponse( - transactionStatus = TransactionStatus.TRANSACTION_NOT_SPECIFIED, - error = Some("unknown") - ) } } @@ -277,23 +208,24 @@ trait PaymentServer extends PaymentServiceApi with GenericPaymentHandler { import in._ val maybeType: Option[RecurringPayment.RecurringPaymentType] = `type` maybeType match { - case Some(atype) => - !?( - RegisterRecurringPayment( - debitedAccount, - firstDebitedAmount, - firstFeesAmount, - currency, - atype, - startDate, - endDate, - frequency, - fixedNextAmount, - nextDebitedAmount, - nextFeesAmount - ) + case Some(recurringPaymentType) => + registerRecurringPayment( + debitedAccount, + firstDebitedAmount, + firstFeesAmount, + currency, + recurringPaymentType, + startDate, + endDate, + frequency, + fixedNextAmount, + nextDebitedAmount, + nextFeesAmount, + statementDescriptor, + externalReference, + Some(clientId) ) map { - case r: RecurringPaymentRegistered => + case Right(r: RecurringPaymentRegistered) => RegisterRecurringPaymentResponse(Some(r.recurringPaymentRegistrationId)) case _ => RegisterRecurringPaymentResponse() } @@ -303,9 +235,9 @@ trait PaymentServer extends PaymentServiceApi with GenericPaymentHandler { override def cancelMandate(in: CancelMandateRequest): Future[CancelMandateResponse] = { import in._ - !?(CancelMandate(externalUuid)) map { - case MandateCanceled => CancelMandateResponse(true) - case _ => CancelMandateResponse() + cancelMandate(externalUuid, Some(clientId)) map { + case Right(r) => CancelMandateResponse(r) + case Left(_) => CancelMandateResponse() } } @@ -313,9 +245,9 @@ trait PaymentServer extends PaymentServiceApi with GenericPaymentHandler { in: LoadBankAccountOwnerRequest ): Future[LoadBankAccountOwnerResponse] = { import in._ - !?(LoadBankAccount(externalUuid)) map { - case r: BankAccountLoaded => - LoadBankAccountOwnerResponse(r.bankAccount.ownerName, Some(r.bankAccount.ownerAddress)) + loadBankAccount(externalUuid, Some(clientId)) map { + case Some(r: BankAccount) => + LoadBankAccountOwnerResponse(r.ownerName, Some(r.ownerAddress)) case _ => LoadBankAccountOwnerResponse() } } @@ -324,9 +256,9 @@ trait PaymentServer extends PaymentServiceApi with GenericPaymentHandler { in: LoadLegalUserRequest ): Future[LoadLegalUserResponse] = { import in._ - !?(LoadPaymentAccount(externalUuid)) map { - case r: PaymentAccountLoaded if r.paymentAccount.user.isLegalUser => - val legalUser = r.paymentAccount.getLegalUser + loadPaymentAccount(externalUuid, Some(clientId)) map { + case Some(r: PaymentAccount) if r.user.isLegalUser => + val legalUser = r.getLegalUser LoadLegalUserResponse( legalUser.legalUserType, legalUser.legalName, @@ -334,8 +266,8 @@ trait PaymentServer extends PaymentServiceApi with GenericPaymentHandler { Some(legalUser.legalRepresentativeAddress), Some(legalUser.headQuartersAddress) ) - case r: PaymentAccountLoaded if r.paymentAccount.bankAccount.isDefined => - val bankAccount = r.paymentAccount.getBankAccount + case Some(r: PaymentAccount) if r.bankAccount.isDefined => + val bankAccount = r.getBankAccount LoadLegalUserResponse( LegalUserType.SOLETRADER, bankAccount.ownerName, @@ -346,4 +278,14 @@ trait PaymentServer extends PaymentServiceApi with GenericPaymentHandler { case _ => LoadLegalUserResponse() } } + +} + +object PaymentServer { + def apply(sys: ActorSystem[_]): PaymentServer = { + new PaymentServer { + lazy val log: Logger = LoggerFactory getLogger getClass.getName + override implicit val system: ActorSystem[_] = sys + } + } } diff --git a/core/src/main/scala/app/softnetwork/payment/handlers/GenericPaymentHandler.scala b/core/src/main/scala/app/softnetwork/payment/handlers/GenericPaymentHandler.scala deleted file mode 100644 index 631e948..0000000 --- a/core/src/main/scala/app/softnetwork/payment/handlers/GenericPaymentHandler.scala +++ /dev/null @@ -1,238 +0,0 @@ -package app.softnetwork.payment.handlers - -import akka.actor.typed.ActorSystem -import app.softnetwork.kv.handlers.GenericKeyValueDao -import app.softnetwork.payment.message.PaymentMessages._ -import app.softnetwork.persistence.typed.scaladsl.EntityPattern -import app.softnetwork.payment.model._ -import app.softnetwork.persistence._ -import app.softnetwork.persistence.typed.CommandTypeKey - -import scala.concurrent.{ExecutionContextExecutor, Future} -import scala.language.implicitConversions -import scala.reflect.ClassTag - -trait GenericPaymentHandler extends EntityPattern[PaymentCommand, PaymentResult] { - _: CommandTypeKey[PaymentCommand] => - lazy val keyValueDao: GenericKeyValueDao = - PaymentKvDao //FIXME app.softnetwork.payment.persistence.data.paymentKvDao - - protected override def lookup[T]( - key: T - )(implicit system: ActorSystem[_]): Future[Option[Recipient]] = { - implicit val ec: ExecutionContextExecutor = system.executionContext - keyValueDao.lookupKeyValue(key) map { - case None => Some(generateUUID(Some(key))) - case some => some - } - } - - override def !?( - command: PaymentCommand - )(implicit tTag: ClassTag[PaymentCommand], system: ActorSystem[_]): Future[PaymentResult] = { - command match { - case cmd: PaymentCommandWithKey => ??(cmd.key, cmd) - case _ => super.!?(command) - } - } - - override def !!( - command: PaymentCommand - )(implicit tTag: ClassTag[PaymentCommand], system: ActorSystem[_]): Unit = { - command match { - case cmd: PaymentCommandWithKey => ?!(cmd.key, cmd) - case _ => super.!!(command) - } - } -} - -trait GenericPaymentDao { _: GenericPaymentHandler => - - protected[payment] def loadPaymentAccount( - key: String - )(implicit system: ActorSystem[_]): Future[Option[PaymentAccount]] = { - implicit val ec: ExecutionContextExecutor = system.executionContext - !?(LoadPaymentAccount(key)) map { - case result: PaymentAccountLoaded => Some(result.paymentAccount) - case _ => None - } - } - - def preRegisterCard(orderUuid: String, user: PaymentUser, currency: String = "EUR")(implicit - system: ActorSystem[_] - ): Future[Option[CardPreRegistration]] = { - implicit val ec: ExecutionContextExecutor = system.executionContext - !?(PreRegisterCard(orderUuid, user, currency)) map { - case result: CardPreRegistered => Some(result.cardPreRegistration) - case _ => None - } - } - - def payIn( - orderUuid: String, - debitedAccount: String, - debitedAmount: Int = 100, - currency: String = "EUR", - creditedAccount: String, - registrationId: Option[String] = None, - registrationData: Option[String] = None, - registerCard: Boolean = false, - ipAddress: Option[String] = None, - browserInfo: Option[BrowserInfo] = None, - statementDescriptor: Option[String] = None, - paymentType: Transaction.PaymentType = Transaction.PaymentType.CARD, - printReceipt: Boolean = false - )(implicit - system: ActorSystem[_] - ): Future[Either[PayInFailed, Either[PaymentRedirection, PaidIn]]] = { - implicit val ec: ExecutionContextExecutor = system.executionContext - !?( - PayIn( - orderUuid, - debitedAccount, - debitedAmount, - currency, - creditedAccount, - registrationId, - registrationData, - registerCard, - ipAddress, - browserInfo, - statementDescriptor, - paymentType, - printReceipt - ) - ) map { - case result: PaymentRedirection => Right(Left(result)) - case result: PaidIn => Right(Right(result)) - case error: PayInFailed => Left(error) - case _ => - Left(PayInFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, "unknown")) - } - } - - def preAuthorizeCard( - orderUuid: String, - debitedAccount: String, - debitedAmount: Int = 100, - currency: String = "EUR", - registrationId: Option[String] = None, - registrationData: Option[String] = None, - registerCard: Boolean = false, - ipAddress: Option[String] = None, - browserInfo: Option[BrowserInfo] = None, - printReceipt: Boolean = false - )(implicit - system: ActorSystem[_] - ): Future[Either[CardPreAuthorizationFailed, Either[PaymentRedirection, CardPreAuthorized]]] = { - implicit val ec: ExecutionContextExecutor = system.executionContext - !?( - PreAuthorizeCard( - orderUuid, - debitedAccount, - debitedAmount, - currency, - registrationId, - registrationData, - registerCard, - ipAddress, - browserInfo, - printReceipt - ) - ) map { - case result: PaymentRedirection => Right(Left(result)) - case result: CardPreAuthorized => Right(Right(result)) - case error: CardPreAuthorizationFailed => Left(error) - case _ => Left(CardPreAuthorizationFailed("unknown")) - } - } - - def payInWithCardPreAuthorized( - preAuthorizationId: String, - creditedAccount: String, - debitedAmount: Option[Int] - )(implicit - system: ActorSystem[_] - ): Future[Either[PayInFailed, PaidIn]] = { - implicit val ec: ExecutionContextExecutor = system.executionContext - !?(PayInWithCardPreAuthorized(preAuthorizationId, creditedAccount, debitedAmount)) map { - case result: PaidIn => Right(result) - case error: PayInFailed => Left(error) - case _ => - Left(PayInFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, "unknown")) - } - } - - def refund( - orderUuid: String, - payInTransactionId: String, - refundAmount: Int, - currency: String = "EUR", - reasonMessage: String, - initializedByClient: Boolean - )(implicit system: ActorSystem[_]): Future[Either[RefundFailed, Refunded]] = { - implicit val ec: ExecutionContextExecutor = system.executionContext - !?( - Refund( - orderUuid, - payInTransactionId, - refundAmount, - currency, - reasonMessage, - initializedByClient - ) - ) map { - case result: Refunded => Right(result) - case error: RefundFailed => Left(error) - case _ => - Left(RefundFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, "unknown")) - } - } - - def payOut( - orderUuid: String, - creditedAccount: String, - creditedAmount: Int, - feesAmount: Int, - currency: String = "EUR", - externalReference: Option[String] - )(implicit - system: ActorSystem[_] - ): Future[Either[PayOutFailed, PaidOut]] = { - implicit val ec: ExecutionContextExecutor = system.executionContext - !?( - PayOut(orderUuid, creditedAccount, creditedAmount, feesAmount, currency, externalReference) - ) map { - case result: PaidOut => Right(result) - case error: PayOutFailed => Left(error) - case _ => - Left(PayOutFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, "unknown")) - } - } - - def transfer( - orderUuid: Option[String] = None, - debitedAccount: String, - creditedAccount: String, - debitedAmount: Int, - feesAmount: Int = 0, - payOutRequired: Boolean = true - )(implicit system: ActorSystem[_]): Future[Either[TransferFailed, Transferred]] = { - implicit val ec: ExecutionContextExecutor = system.executionContext - !?( - Transfer( - orderUuid, - debitedAccount, - creditedAccount, - debitedAmount, - feesAmount, - payOutRequired - ) - ) map { - case result: Transferred => Right(result) - case error: TransferFailed => Left(error) - case _ => - Left(TransferFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, "unknown")) - } - } -} diff --git a/core/src/main/scala/app/softnetwork/payment/handlers/PaymentDao.scala b/core/src/main/scala/app/softnetwork/payment/handlers/PaymentDao.scala new file mode 100644 index 0000000..a379c18 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/handlers/PaymentDao.scala @@ -0,0 +1,453 @@ +package app.softnetwork.payment.handlers + +import akka.actor.typed.ActorSystem +import akka.cluster.sharding.typed.scaladsl.EntityTypeKey +import app.softnetwork.kv.handlers.GenericKeyValueDao +import app.softnetwork.payment.annotation.InternalApi +import app.softnetwork.payment.message.PaymentMessages.{MandateCanceled, _} +import app.softnetwork.persistence.typed.scaladsl.EntityPattern +import app.softnetwork.payment.model._ +import app.softnetwork.payment.persistence.typed.PaymentBehavior +import app.softnetwork.persistence._ +import app.softnetwork.persistence.typed.CommandTypeKey +import org.slf4j.{Logger, LoggerFactory} + +import java.util.Date +import scala.concurrent.{ExecutionContextExecutor, Future} +import scala.language.implicitConversions +import scala.reflect.ClassTag + +trait PaymentTypeKey extends CommandTypeKey[PaymentCommand] { + override def TypeKey(implicit tTag: ClassTag[PaymentCommand]): EntityTypeKey[PaymentCommand] = + PaymentBehavior.TypeKey +} + +trait PaymentHandler extends EntityPattern[PaymentCommand, PaymentResult] with PaymentTypeKey { + lazy val keyValueDao: GenericKeyValueDao = + PaymentKvDao //FIXME app.softnetwork.payment.persistence.data.paymentKvDao + + protected override def lookup[T]( + key: T + )(implicit system: ActorSystem[_]): Future[Option[Recipient]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + keyValueDao.lookupKeyValue(key) map { + case None => Some(generateUUID(Some(key))) + case some => some + } + } + + override def !?( + command: PaymentCommand + )(implicit tTag: ClassTag[PaymentCommand], system: ActorSystem[_]): Future[PaymentResult] = { + command match { + case cmd: PaymentCommandWithKey => ??(cmd.key, cmd) + case _ => super.!?(command) + } + } + + override def !!( + command: PaymentCommand + )(implicit tTag: ClassTag[PaymentCommand], system: ActorSystem[_]): Unit = { + command match { + case cmd: PaymentCommandWithKey => ?!(cmd.key, cmd) + case _ => super.!!(command) + } + } +} + +trait PaymentDao extends PaymentHandler { + + @InternalApi + private[payment] def loadPaymentAccount( + key: String, + clientId: Option[String] + )(implicit system: ActorSystem[_]): Future[Option[PaymentAccount]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?(LoadPaymentAccount(key, clientId)) map { + case result: PaymentAccountLoaded => Some(result.paymentAccount) + case _ => None + } + } + + @InternalApi + private[payment] def loadBankAccount( + externalUuid: String, + clientId: Option[String] = None + )(implicit system: ActorSystem[_]): Future[Option[BankAccount]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?(LoadBankAccount(externalUuid, clientId)) map { + case result: BankAccountLoaded => Some(result.bankAccount) + case _ => None + } + } + + @InternalApi + private[payment] def preRegisterCard( + orderUuid: String, + user: NaturalUser, + currency: String = "EUR", + clientId: Option[String] = None + )(implicit + system: ActorSystem[_] + ): Future[Either[String, CardPreRegistration]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?(PreRegisterCard(orderUuid, user, currency, clientId)) map { + case result: CardPreRegistered => Right(result.cardPreRegistration) + case error: PaymentError => Left(error.message) + case _ => Left("unknown") + } + } + + def payIn( + orderUuid: String, + debitedAccount: String, + debitedAmount: Int = 100, + currency: String = "EUR", + creditedAccount: String, + registrationId: Option[String] = None, + registrationData: Option[String] = None, + registerCard: Boolean = false, + ipAddress: Option[String] = None, + browserInfo: Option[BrowserInfo] = None, + statementDescriptor: Option[String] = None, + paymentType: Transaction.PaymentType = Transaction.PaymentType.CARD, + printReceipt: Boolean = false + )(implicit + system: ActorSystem[_] + ): Future[Either[PayInFailed, Either[PaymentRedirection, PaidIn]]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?( + PayIn( + orderUuid, + debitedAccount, + debitedAmount, + currency, + creditedAccount, + registrationId, + registrationData, + registerCard, + ipAddress, + browserInfo, + statementDescriptor, + paymentType, + printReceipt + ) + ) map { + case result: PaymentRedirection => Right(Left(result)) + case result: PaidIn => Right(Right(result)) + case error: PayInFailed => Left(error) + case _ => + Left(PayInFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, "unknown")) + } + } + + def preAuthorizeCard( + orderUuid: String, + debitedAccount: String, + debitedAmount: Int = 100, + currency: String = "EUR", + registrationId: Option[String] = None, + registrationData: Option[String] = None, + registerCard: Boolean = false, + ipAddress: Option[String] = None, + browserInfo: Option[BrowserInfo] = None, + printReceipt: Boolean = false + )(implicit + system: ActorSystem[_] + ): Future[Either[CardPreAuthorizationFailed, Either[PaymentRedirection, CardPreAuthorized]]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?( + PreAuthorizeCard( + orderUuid, + debitedAccount, + debitedAmount, + currency, + registrationId, + registrationData, + registerCard, + ipAddress, + browserInfo, + printReceipt + ) + ) map { + case result: PaymentRedirection => Right(Left(result)) + case result: CardPreAuthorized => Right(Right(result)) + case error: CardPreAuthorizationFailed => Left(error) + case _ => Left(CardPreAuthorizationFailed("unknown")) + } + } + + @InternalApi + private[payment] def cancelPreAuthorization( + orderUuid: String, + cardPreAuthorizedTransactionId: String, + clientId: Option[String] = None + )(implicit system: ActorSystem[_]): Future[Either[String, PreAuthorizationCanceled]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?(CancelPreAuthorization(orderUuid, cardPreAuthorizedTransactionId, clientId)) map { + case result: PreAuthorizationCanceled => Right(result) + case error: PaymentError => Left(error.message) + case _ => Left("unknown") + } + } + + @InternalApi + private[payment] def payInWithCardPreAuthorized( + preAuthorizationId: String, + creditedAccount: String, + debitedAmount: Option[Int], + clientId: Option[String] = None + )(implicit + system: ActorSystem[_] + ): Future[Either[PayInFailed, PaidIn]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?( + PayInWithCardPreAuthorized(preAuthorizationId, creditedAccount, debitedAmount, clientId) + ) map { + case result: PaidIn => Right(result) + case error: PayInFailed => Left(error) + case error: PaymentError => + Left( + PayInFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, error.message) + ) + case _ => + Left(PayInFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, "unknown")) + } + } + + @InternalApi + private[payment] def refund( + orderUuid: String, + payInTransactionId: String, + refundAmount: Int, + currency: String = "EUR", + reasonMessage: String, + initializedByClient: Boolean, + clientId: Option[String] = None + )(implicit system: ActorSystem[_]): Future[Either[RefundFailed, Refunded]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?( + Refund( + orderUuid, + payInTransactionId, + refundAmount, + currency, + reasonMessage, + initializedByClient, + clientId + ) + ) map { + case result: Refunded => Right(result) + case error: RefundFailed => Left(error) + case error: PaymentError => + Left( + RefundFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, error.message) + ) + case _ => + Left(RefundFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, "unknown")) + } + } + + @InternalApi + private[payment] def payOut( + orderUuid: String, + creditedAccount: String, + creditedAmount: Int, + feesAmount: Int, + currency: String = "EUR", + externalReference: Option[String], + clientId: Option[String] = None + )(implicit + system: ActorSystem[_] + ): Future[Either[PayOutFailed, PaidOut]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?( + PayOut( + orderUuid, + creditedAccount, + creditedAmount, + feesAmount, + currency, + externalReference, + clientId + ) + ) map { + case result: PaidOut => Right(result) + case error: PayOutFailed => Left(error) + case error: PaymentError => + Left( + PayOutFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, error.message) + ) + case _ => + Left(PayOutFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, "unknown")) + } + } + + @InternalApi + private[payment] def transfer( + orderUuid: Option[String] = None, + debitedAccount: String, + creditedAccount: String, + debitedAmount: Int, + feesAmount: Int = 0, + currency: String = "EUR", + payOutRequired: Boolean = true, + externalReference: Option[String] = None, + clientId: Option[String] = None + )(implicit system: ActorSystem[_]): Future[Either[TransferFailed, Transferred]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?( + Transfer( + orderUuid, + debitedAccount, + creditedAccount, + debitedAmount, + feesAmount, + currency, + payOutRequired, + externalReference, + clientId + ) + ) map { + case result: Transferred => Right(result) + case error: TransferFailed => Left(error) + case error: PaymentError => + Left( + TransferFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, error.message) + ) + case _ => + Left(TransferFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, "unknown")) + } + } + + @InternalApi + private[payment] def cancelMandate( + creditedAccount: String, + clientId: Option[String] = None + )(implicit system: ActorSystem[_]): Future[Either[String, Boolean]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?(CancelMandate(creditedAccount, clientId)) map { + case MandateCanceled => Right(true) + case error: PaymentError => Left(error.message) + case _ => Left("unknown") + } + } + + @InternalApi + private[payment] def directDebit( + creditedAccount: String, + debitedAmount: Int, + feesAmount: Int = 0, + currency: String = "EUR", + statementDescriptor: String, + externalReference: Option[String] = None, + clientId: Option[String] = None + )(implicit + system: ActorSystem[_] + ): Future[Either[DirectDebitFailed, DirectDebited]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?( + DirectDebit( + creditedAccount, + debitedAmount, + feesAmount, + currency, + statementDescriptor, + externalReference, + clientId + ) + ) map { + case result: DirectDebited => Right(result) + case error: DirectDebitFailed => Left(error) + case error: PaymentError => + Left( + DirectDebitFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + error.message + ) + ) + case _ => + Left( + DirectDebitFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, "unknown") + ) + } + } + + @InternalApi + private[payment] def loadDirectDebitTransaction( + directDebitTransactionId: String, + clientId: Option[String] = None + )(implicit + system: ActorSystem[_] + ): Future[Either[DirectDebitFailed, DirectDebited]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?(LoadDirectDebitTransaction(directDebitTransactionId, clientId)) map { + case result: DirectDebited => Right(result) + case error: DirectDebitFailed => Left(error) + case error: PaymentError => + Left( + DirectDebitFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + error.message + ) + ) + case _ => + Left( + DirectDebitFailed("", Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, "unknown") + ) + } + } + + @InternalApi + private[payment] def registerRecurringPayment( + debitedAccount: String, + firstDebitedAmount: Int, + firstFeesAmount: Int, + currency: String = "EUR", + `type`: RecurringPayment.RecurringPaymentType = RecurringPayment.RecurringPaymentType.CARD, + startDate: Option[Date] = None, + endDate: Option[Date] = None, + frequency: Option[RecurringPayment.RecurringPaymentFrequency] = None, + fixedNextAmount: Option[Boolean] = None, + nextDebitedAmount: Option[Int] = None, + nextFeesAmount: Option[Int] = None, + statementDescriptor: Option[String] = None, + externalReference: Option[String] = None, + clientId: Option[String] = None + )(implicit + system: ActorSystem[_] + ): Future[Either[String, RecurringPaymentRegistered]] = { + implicit val ec: ExecutionContextExecutor = system.executionContext + !?( + RegisterRecurringPayment( + debitedAccount, + firstDebitedAmount, + firstFeesAmount, + currency, + `type`, + startDate, + endDate, + frequency, + fixedNextAmount, + nextDebitedAmount, + nextFeesAmount, + statementDescriptor, + externalReference, + Some(clientId) + ) + ) map { + case result: RecurringPaymentRegistered => Right(result) + case error: PaymentError => Left(error.message) + case _ => + Left( + "unknown" + ) + } + } +} + +object PaymentDao extends PaymentDao { + lazy val log: Logger = LoggerFactory.getLogger(getClass.getName) +} diff --git a/core/src/main/scala/app/softnetwork/payment/handlers/SoftPayAccountDao.scala b/core/src/main/scala/app/softnetwork/payment/handlers/SoftPayAccountDao.scala new file mode 100644 index 0000000..07e621f --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/handlers/SoftPayAccountDao.scala @@ -0,0 +1,221 @@ +package app.softnetwork.payment.handlers + +import akka.actor.typed.ActorSystem +import akka.cluster.sharding.typed.scaladsl.EntityTypeKey +import app.softnetwork.account.handlers.{AccountDao, AccountHandler} +import app.softnetwork.account.message.{ + AccessTokenGenerated, + AccessTokenRefreshed, + AccountActivated, + AccountCommand, + AccountCreated, + AccountErrorMessage, + Activate, + Tokens +} +import app.softnetwork.payment.annotation.InternalApi +import app.softnetwork.payment.message.AccountMessages +import app.softnetwork.payment.model.SoftPayAccount +import app.softnetwork.payment.persistence.typed.SoftPayAccountBehavior +import app.softnetwork.persistence.generateUUID +import app.softnetwork.persistence.typed.CommandTypeKey +import app.softnetwork.session.model.JwtClaimsEncoder +import com.softwaremill.session.SessionConfig +import org.softnetwork.session.model.{ApiKey, JwtClaims} +import org.slf4j.{Logger, LoggerFactory} + +import scala.concurrent.{ExecutionContext, Future} +import scala.reflect.ClassTag + +trait SoftPayAccountDao extends AccountDao with SoftPayAccountHandler { + @InternalApi + private[payment] def loadProvider( + clientId: String + )(implicit + system: ActorSystem[_] + ): Future[Option[SoftPayAccount.Client.Provider]] = { + implicit val ec: ExecutionContext = system.executionContext + loadClient(clientId).map(_.map(_.provider)) + } + + @InternalApi + private[payment] def loadClient( + clientId: String + )(implicit system: ActorSystem[_]): Future[Option[SoftPayAccount.Client]] = { + implicit val ec: ExecutionContext = system.executionContext + ??(clientId, AccountMessages.LoadClient(clientId)) map { + case result: AccountMessages.ClientLoaded => Some(result.client) + case _ => None + } + } + + @InternalApi + private[payment] def oauthClient( + token: String + )(implicit system: ActorSystem[_]): Future[Option[SoftPayAccount.Client]] = { + implicit val ec: ExecutionContext = system.executionContext + ??(token, AccountMessages.OAuthClient(token)) map { + case result: AccountMessages.OAuthClientSucceededResult => Some(result.client) + case _ => None + } + } + + @InternalApi + private[payment] def generateClientTokens( + clientId: String, + clientSecret: String, + scope: Option[String] = None + )(implicit system: ActorSystem[_]): Future[Either[String, Tokens]] = { + implicit val ec: ExecutionContext = system.executionContext + ??(clientId, AccountMessages.GenerateClientToken(clientId, clientSecret, scope)) map { + case result: AccessTokenGenerated => + import result._ + Right( + Tokens( + accessToken.token, + accessToken.tokenType.toLowerCase(), + accessToken.expiresIn, + accessToken.refreshToken, + accessToken.refreshExpiresIn + ) + ) + case error: AccountErrorMessage => Left(error.message) + case _ => Left("unknown") + } + } + + @InternalApi + private[payment] def refreshClientTokens( + refreshToken: String + )(implicit system: ActorSystem[_]): Future[Either[String, Tokens]] = { + implicit val ec: ExecutionContext = system.executionContext + ??(refreshToken, AccountMessages.RefreshClientToken(refreshToken)) map { + case result: AccessTokenRefreshed => + import result._ + Right( + Tokens( + accessToken.token, + accessToken.tokenType.toLowerCase(), + accessToken.expiresIn, + accessToken.refreshToken, + accessToken.refreshExpiresIn + ) + ) + case error: AccountErrorMessage => Left(error.message) + case _ => Left("unknown") + } + } + + @InternalApi + private[payment] def signUpClient( + signUp: AccountMessages.SoftPaySignUp + )(implicit system: ActorSystem[_]): Future[Either[String, SoftPayAccount.Client]] = { + implicit val ec: ExecutionContext = system.executionContext + ??( + generateUUID(Some(signUp.login)), + signUp + ) map { + case result: AccountCreated => + Right( + result.account + .asInstanceOf[SoftPayAccount] + .clients + .find(_.provider.providerId == signUp.provider.providerId) + .get + ) + case error: AccountErrorMessage => Left(error.message) + case _ => Left("unknown") + } + } + + @InternalApi + private[payment] def activateClient( + activation: Activate + )(implicit system: ActorSystem[_]): Future[Either[String, Boolean]] = { + implicit val ec: ExecutionContext = system.executionContext + ??(activation.token, activation) map { + case result: AccountActivated => Right(result.account.status.isActive) + case error: AccountErrorMessage => Left(error.message) + case _ => Left("unknown") + } + } + + @InternalApi + private[payment] def loadApiKey( + clientId: String + )(implicit system: ActorSystem[_]): Future[Option[ApiKey]] = { + implicit val ec: ExecutionContext = system.executionContext + ??(clientId, AccountMessages.LoadApiKey(clientId)) map { + case result: AccountMessages.ApiKeyLoaded => Some(result.apiKey) + case _ => None + } + } + + @InternalApi + private[payment] def registerAccountWithProvider( + provider: SoftPayAccount.Client.Provider + )(implicit + system: ActorSystem[_] + ): Future[Option[SoftPayAccount]] = { + implicit val ec: ExecutionContext = system.executionContext + !?(AccountMessages.RegisterAccountWithProvider(provider)) map { + case result: AccountMessages.AccountWithProviderRegistered => Some(result.account) + case _ => None + } + } + + @InternalApi + private[payment] def authenticateClient( + token: Option[String] + )(implicit system: ActorSystem[_]): Future[Option[SoftPayAccount.Client]] = { + token match { + case Some(value) => + implicit val ec: ExecutionContext = system.executionContext + oauthClient(value) flatMap { + case Some(client) => Future.successful(Some(client)) + case _ => + val t = JwtClaims(value) + t.clientId match { + case Some(clientId) => + loadApiKey(clientId) flatMap { + case Some(apiKey) if apiKey.clientSecret.isDefined => + val config = SessionConfig.default(apiKey.getClientSecret) + JwtClaimsEncoder + .decode( + value, + config.copy(jwt = + config.jwt.copy( + issuer = t.issuer.orElse(config.jwt.issuer), + subject = t.subject.orElse(config.jwt.subject), + audience = t.aud.orElse(config.jwt.audience) + ) + ) + ) + .toOption match { + case Some(result) if result.signatureMatches => + loadClient(clientId) flatMap { + case None => Future.successful(None) + case some => Future.successful(some) + } + case _ => Future.successful(None) + } + case _ => Future.successful(None) + } + case _ => Future.successful(None) + } + } + case _ => Future.successful(None) + } + } +} + +trait SoftPayAccountTypeKey extends CommandTypeKey[AccountCommand] { + override def TypeKey(implicit tTag: ClassTag[AccountCommand]): EntityTypeKey[AccountCommand] = + SoftPayAccountBehavior.TypeKey +} + +trait SoftPayAccountHandler extends AccountHandler with SoftPayAccountTypeKey + +object SoftPayAccountDao extends SoftPayAccountDao { + lazy val log: Logger = LoggerFactory getLogger getClass.getName +} diff --git a/core/src/main/scala/app/softnetwork/payment/launch/PaymentApplication.scala b/core/src/main/scala/app/softnetwork/payment/launch/PaymentApplication.scala index b52b13e..843d8df 100644 --- a/core/src/main/scala/app/softnetwork/payment/launch/PaymentApplication.scala +++ b/core/src/main/scala/app/softnetwork/payment/launch/PaymentApplication.scala @@ -2,10 +2,15 @@ package app.softnetwork.payment.launch import app.softnetwork.api.server.ApiRoutes import app.softnetwork.api.server.launch.Application +import app.softnetwork.payment.api.PaymentGrpcServices import app.softnetwork.persistence.schema.SchemaProvider import app.softnetwork.session.CsrfCheck -trait PaymentApplication extends Application with ApiRoutes with PaymentGuardian { +trait PaymentApplication + extends Application + with ApiRoutes + with PaymentGuardian + with PaymentGrpcServices { _: SchemaProvider with CsrfCheck => override val applicationVersion = systemVersion() } diff --git a/core/src/main/scala/app/softnetwork/payment/launch/PaymentEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/launch/PaymentEndpoints.scala index e94538d..eeeb0a4 100644 --- a/core/src/main/scala/app/softnetwork/payment/launch/PaymentEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/launch/PaymentEndpoints.scala @@ -1,21 +1,36 @@ package app.softnetwork.payment.launch import akka.actor.typed.ActorSystem +import app.softnetwork.account.launch.AccountEndpoints +import app.softnetwork.account.message.BasicAccountSignUp +import app.softnetwork.account.model.BasicAccountProfile import app.softnetwork.api.server.{ApiEndpoints, Endpoint} +import app.softnetwork.payment.model.SoftPayAccount import app.softnetwork.payment.serialization.paymentFormats -import app.softnetwork.payment.service.GenericPaymentEndpoints +import app.softnetwork.payment.service.PaymentServiceEndpoints import app.softnetwork.persistence.schema.SchemaProvider +import app.softnetwork.session.CsrfCheck +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} import org.json4s.Formats -trait PaymentEndpoints extends ApiEndpoints { _: PaymentGuardian with SchemaProvider => +trait PaymentEndpoints[SD <: SessionData with SessionDataDecorator[SD]] + extends AccountEndpoints[ + SoftPayAccount, + BasicAccountProfile, + BasicAccountSignUp, + SD + ] { + _: PaymentGuardian with SchemaProvider with CsrfCheck => override implicit def formats: Formats = paymentFormats - def paymentEndpoints: ActorSystem[_] => GenericPaymentEndpoints + def paymentEndpoints: ActorSystem[_] => PaymentServiceEndpoints[SD] override def endpoints: ActorSystem[_] => List[Endpoint] = system => List( - paymentEndpoints(system) + paymentEndpoints(system), + accountEndpoints(system), + oauthEndpoints(system) ) } diff --git a/core/src/main/scala/app/softnetwork/payment/launch/PaymentGuardian.scala b/core/src/main/scala/app/softnetwork/payment/launch/PaymentGuardian.scala index 2e42862..541c3c2 100644 --- a/core/src/main/scala/app/softnetwork/payment/launch/PaymentGuardian.scala +++ b/core/src/main/scala/app/softnetwork/payment/launch/PaymentGuardian.scala @@ -1,41 +1,61 @@ package app.softnetwork.payment.launch import akka.actor.typed.ActorSystem +import app.softnetwork.account.handlers.AccountDao +import app.softnetwork.account.launch.AccountGuardian +import app.softnetwork.account.model.BasicAccountProfile +import app.softnetwork.account.persistence.typed.AccountBehavior +import app.softnetwork.api.server.GrpcService import app.softnetwork.payment.PaymentCoreBuildInfo +import app.softnetwork.payment.api.{ + ClientGrpcService, + ClientServer, + PaymentGrpcService, + PaymentServer +} +import app.softnetwork.payment.handlers.SoftPayAccountDao +import app.softnetwork.payment.model.SoftPayAccount import app.softnetwork.payment.persistence.data.paymentKvDao import app.softnetwork.payment.persistence.query.{ - GenericPaymentCommandProcessorStream, + PaymentCommandProcessorStream, Scheduler2PaymentProcessorStream } -import app.softnetwork.payment.persistence.typed.GenericPaymentBehavior +import app.softnetwork.payment.persistence.typed.{PaymentBehavior, SoftPayAccountBehavior} +import app.softnetwork.payment.spi.PaymentProviders import app.softnetwork.persistence.launch.PersistentEntity import app.softnetwork.persistence.query.EventProcessorStream import app.softnetwork.persistence.schema.SchemaProvider import app.softnetwork.persistence.typed.Singleton import app.softnetwork.session.CsrfCheck -import app.softnetwork.session.launch.SessionGuardian -trait PaymentGuardian extends SessionGuardian { _: SchemaProvider with CsrfCheck => +import scala.concurrent.ExecutionContext + +trait PaymentGuardian extends AccountGuardian[SoftPayAccount, BasicAccountProfile] { + _: SchemaProvider with CsrfCheck => import app.softnetwork.persistence.launch.PersistenceGuardian._ - def paymentAccountBehavior: ActorSystem[_] => GenericPaymentBehavior + def paymentBehavior: ActorSystem[_] => PaymentBehavior = _ => PaymentBehavior + + override def accountBehavior + : ActorSystem[_] => AccountBehavior[SoftPayAccount, BasicAccountProfile] = _ => + SoftPayAccountBehavior def paymentEntities: ActorSystem[_] => Seq[PersistentEntity[_, _, _, _]] = sys => Seq( - paymentAccountBehavior(sys) + paymentBehavior(sys) ) /** initialize all entities */ override def entities: ActorSystem[_] => Seq[PersistentEntity[_, _, _, _]] = sys => - sessionEntities(sys) ++ paymentEntities(sys) + sessionEntities(sys) ++ accountEntities(sys) ++ paymentEntities(sys) /** initialize all singletons */ override def singletons: ActorSystem[_] => Seq[Singleton[_]] = _ => Seq(paymentKvDao) - def paymentCommandProcessorStream: ActorSystem[_] => GenericPaymentCommandProcessorStream + def paymentCommandProcessorStream: ActorSystem[_] => PaymentCommandProcessorStream def scheduler2PaymentProcessorStream: ActorSystem[_] => Scheduler2PaymentProcessorStream @@ -45,8 +65,54 @@ trait PaymentGuardian extends SessionGuardian { _: SchemaProvider with CsrfCheck /** initialize all event processor streams */ override def eventProcessorStreams: ActorSystem[_] => Seq[EventProcessorStream[_]] = sys => - paymentEventProcessorStreams(sys) + paymentEventProcessorStreams(sys) ++ accountEventProcessorStreams(sys) override def systemVersion(): String = sys.env.getOrElse("VERSION", PaymentCoreBuildInfo.version) + + def softPayAccountDao: SoftPayAccountDao = SoftPayAccountDao + + final override def accountDao: AccountDao = softPayAccountDao + + def paymentServer: ActorSystem[_] => PaymentServer = system => PaymentServer(system) + + def clientServer: ActorSystem[_] => ClientServer = system => ClientServer(system) + + def paymentGrpcServices: ActorSystem[_] => Seq[GrpcService] = system => + Seq( + new PaymentGrpcService(paymentServer(system), softPayAccountDao), + new ClientGrpcService(clientServer(system)) + ) + + def registerProvidersAccount: ActorSystem[_] => Unit = system => { + PaymentProviders.defaultPaymentProviders.foreach(provider => { + implicit val ec: ExecutionContext = system.executionContext + softPayAccountDao.registerAccountWithProvider(provider)(system) map { + case Some(account) => + system.log.info(s"Registered provider account for ${provider.providerId}: $account") + case _ => + system.log.warn(s"Failed to register provider account for ${provider.providerId}") + } + }) + } + + override def initSystem: ActorSystem[_] => Unit = system => { + registerProvidersAccount(system) + super.initSystem(system) + } + + override def banner: String = + """ + |█████████ ██████ █████ ███████████ █████ + | ███░░░░░███ ███░░███ ░░███ ░░███░░░░░███ ░░███ + |░███ ░░░ ██████ ░███ ░░░ ███████ ░███ ░███ ██████ █████ ████ █████████████ ██████ ████████ ███████ + |░░█████████ ███░░███ ███████ ░░░███░ ░██████████ ░░░░░███ ░░███ ░███ ░░███░░███░░███ ███░░███░░███░░███ ░░░███░ + | ░░░░░░░░███░███ ░███░░░███░ ░███ ░███░░░░░░ ███████ ░███ ░███ ░███ ░███ ░███ ░███████ ░███ ░███ ░███ + | ███ ░███░███ ░███ ░███ ░███ ███ ░███ ███░░███ ░███ ░███ ░███ ░███ ░███ ░███░░░ ░███ ░███ ░███ ███ + |░░█████████ ░░██████ █████ ░░█████ █████ ░░████████ ░░███████ █████░███ █████░░██████ ████ █████ ░░█████ + | ░░░░░░░░░ ░░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░░░░ ░░░░░███ ░░░░░ ░░░ ░░░░░ ░░░░░░ ░░░░ ░░░░░ ░░░░░ + | ███ ░███ + | ░░██████ + | ░░░░░░ + |""".stripMargin } diff --git a/core/src/main/scala/app/softnetwork/payment/launch/PaymentRoutes.scala b/core/src/main/scala/app/softnetwork/payment/launch/PaymentRoutes.scala index dae3de2..971f2bb 100644 --- a/core/src/main/scala/app/softnetwork/payment/launch/PaymentRoutes.scala +++ b/core/src/main/scala/app/softnetwork/payment/launch/PaymentRoutes.scala @@ -1,22 +1,42 @@ package app.softnetwork.payment.launch import akka.actor.typed.ActorSystem -import app.softnetwork.api.server.{ApiRoute, ApiRoutes} +import app.softnetwork.account.launch.AccountRoutes +import app.softnetwork.account.model.{ + BasicAccountProfile, + DefaultAccountDetailsView, + DefaultAccountView, + DefaultProfileView +} +import app.softnetwork.api.server.ApiRoute +import app.softnetwork.payment.model.SoftPayAccount import app.softnetwork.payment.serialization.paymentFormats -import app.softnetwork.payment.service.GenericPaymentService +import app.softnetwork.payment.service.PaymentService import app.softnetwork.persistence.schema.SchemaProvider +import app.softnetwork.session.CsrfCheck +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} import org.json4s.Formats -trait PaymentRoutes extends ApiRoutes { _: PaymentGuardian with SchemaProvider => +trait PaymentRoutes[SD <: SessionData with SessionDataDecorator[SD]] + extends AccountRoutes[ + SoftPayAccount, + BasicAccountProfile, + DefaultProfileView, + DefaultAccountDetailsView, + DefaultAccountView[DefaultProfileView, DefaultAccountDetailsView], + SD + ] { _: PaymentGuardian with SchemaProvider with CsrfCheck => override implicit def formats: Formats = paymentFormats - def paymentService: ActorSystem[_] => GenericPaymentService + def paymentService: ActorSystem[_] => PaymentService[SD] override def apiRoutes: ActorSystem[_] => List[ApiRoute] = system => List( - paymentService(system) + paymentService(system), + accountService(system), + oauthService(system) ) } diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/query/GenericPaymentCommandProcessorStream.scala b/core/src/main/scala/app/softnetwork/payment/persistence/query/PaymentCommandProcessorStream.scala similarity index 87% rename from core/src/main/scala/app/softnetwork/payment/persistence/query/GenericPaymentCommandProcessorStream.scala rename to core/src/main/scala/app/softnetwork/payment/persistence/query/PaymentCommandProcessorStream.scala index 48c93d7..1ac71af 100644 --- a/core/src/main/scala/app/softnetwork/payment/persistence/query/GenericPaymentCommandProcessorStream.scala +++ b/core/src/main/scala/app/softnetwork/payment/persistence/query/PaymentCommandProcessorStream.scala @@ -3,7 +3,7 @@ package app.softnetwork.payment.persistence.query import akka.Done import akka.actor.typed.eventstream.EventStream.Publish import akka.persistence.typed.PersistenceId -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentEvents._ import app.softnetwork.payment.message.PaymentMessages._ import app.softnetwork.payment.config.PaymentSettings @@ -11,8 +11,8 @@ import app.softnetwork.persistence.query.{EventProcessorStream, JournalProvider, import scala.concurrent.Future -trait GenericPaymentCommandProcessorStream extends EventProcessorStream[PaymentEventWithCommand] { - _: JournalProvider with OffsetProvider with GenericPaymentHandler => +trait PaymentCommandProcessorStream extends EventProcessorStream[PaymentEventWithCommand] { + _: JournalProvider with OffsetProvider with PaymentHandler => override lazy val tag: String = PaymentSettings.ExternalToPaymentAccountTag @@ -58,7 +58,12 @@ trait GenericPaymentCommandProcessorStream extends EventProcessorStream[PaymentE case evt: PayInWithCardPreAuthorizedCommandEvent => import evt._ val command = - PayInWithCardPreAuthorized(preAuthorizationId, creditedAccount, debitedAmount) + PayInWithCardPreAuthorized( + preAuthorizationId, + creditedAccount, + debitedAmount, + clientId + ) !?(command) map { case _: PaidInResult => if (forTests) system.eventStream.tell(Publish(event)) @@ -77,7 +82,8 @@ trait GenericPaymentCommandProcessorStream extends EventProcessorStream[PaymentE refundAmount, currency, reasonMessage, - initializedByClient + initializedByClient, + clientId ) !?(command) map { case _: Refunded => @@ -97,7 +103,8 @@ trait GenericPaymentCommandProcessorStream extends EventProcessorStream[PaymentE creditedAmount, feesAmount, currency, - externalReference + externalReference, + clientId ) !?(command) map { case _: PaidOut => @@ -119,7 +126,8 @@ trait GenericPaymentCommandProcessorStream extends EventProcessorStream[PaymentE feesAmount, currency, payOutRequired, - externalReference + externalReference, + clientId ) !?(command) map { case _: Transferred => @@ -139,7 +147,8 @@ trait GenericPaymentCommandProcessorStream extends EventProcessorStream[PaymentE feesAmount, currency, statementDescriptor, - externalReference + externalReference, + clientId ) !?(command) map { case _: DirectDebited => @@ -153,7 +162,10 @@ trait GenericPaymentCommandProcessorStream extends EventProcessorStream[PaymentE } case evt: LoadDirectDebitTransactionCommandEvent => import evt._ - val command = LoadDirectDebitTransaction(directDebitTransactionId) + val command = LoadDirectDebitTransaction( + directDebitTransactionId, + clientId + ) !?(command) map { case _: DirectDebited => if (forTests) system.eventStream.tell(Publish(event)) @@ -177,7 +189,8 @@ trait GenericPaymentCommandProcessorStream extends EventProcessorStream[PaymentE frequency, fixedNextAmount, nextDebitedAmount, - nextFeesAmount + nextFeesAmount, + clientId ) !?(command) map { case _: RecurringPaymentRegistered => @@ -191,7 +204,11 @@ trait GenericPaymentCommandProcessorStream extends EventProcessorStream[PaymentE } case evt: CancelPreAuthorizationCommandEvent => import evt._ - val command = CancelPreAuthorization(orderUuid, cardPreAuthorizedTransactionId) + val command = CancelPreAuthorization( + orderUuid, + cardPreAuthorizedTransactionId, + clientId + ) !?(command) map { case _: PreAuthorizationCanceled => if (forTests) system.eventStream.tell(Publish(event)) @@ -204,7 +221,7 @@ trait GenericPaymentCommandProcessorStream extends EventProcessorStream[PaymentE } case evt: CancelMandateCommandEvent => import evt._ - val command = CancelMandate(externalUuid) + val command = CancelMandate(externalUuid, clientId) !?(command) map { case MandateCanceled => if (forTests) system.eventStream.tell(Publish(event)) diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/query/Scheduler2PaymentProcessorStream.scala b/core/src/main/scala/app/softnetwork/payment/persistence/query/Scheduler2PaymentProcessorStream.scala index 0454cd6..8a65d04 100644 --- a/core/src/main/scala/app/softnetwork/payment/persistence/query/Scheduler2PaymentProcessorStream.scala +++ b/core/src/main/scala/app/softnetwork/payment/persistence/query/Scheduler2PaymentProcessorStream.scala @@ -1,7 +1,7 @@ package app.softnetwork.payment.persistence.query import akka.actor.typed.eventstream.EventStream.Publish -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages.{ PaymentCommand, PaymentResult, @@ -16,7 +16,7 @@ import scala.concurrent.Future trait Scheduler2PaymentProcessorStream extends Scheduler2EntityProcessorStream[PaymentCommand, PaymentResult] { - _: GenericPaymentHandler with JournalProvider with OffsetProvider => + _: PaymentHandler with JournalProvider with OffsetProvider => /** @param schedule * - the schedule to trigger diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/typed/GenericPaymentBehavior.scala b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala similarity index 93% rename from core/src/main/scala/app/softnetwork/payment/persistence/typed/GenericPaymentBehavior.scala rename to core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala index dddc440..5ab7ea1 100644 --- a/core/src/main/scala/app/softnetwork/payment/persistence/typed/GenericPaymentBehavior.scala +++ b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala @@ -1,27 +1,26 @@ package app.softnetwork.payment.persistence.typed -import akka.actor.typed.{ActorRef, ActorSystem} import akka.actor.typed.scaladsl.{ActorContext, TimerScheduler} +import akka.actor.typed.{ActorRef, ActorSystem} import akka.cluster.sharding.typed.ShardingEnvelope import akka.persistence.typed.scaladsl.Effect import app.softnetwork.kv.handlers.GenericKeyValueDao +import app.softnetwork.payment.annotation.InternalApi +import app.softnetwork.payment.api.config.SoftPayClientSettings import app.softnetwork.payment.config.PaymentSettings import app.softnetwork.payment.config.PaymentSettings.{AkkaNodeRole, PayInStatementDescriptor} -import app.softnetwork.payment.handlers.{GenericPaymentDao, PaymentKvDao} +import app.softnetwork.payment.handlers.{PaymentDao, PaymentKvDao, SoftPayAccountDao} import app.softnetwork.payment.message.PaymentEvents._ import app.softnetwork.payment.message.PaymentMessages._ import app.softnetwork.payment.message.TransactionEvents._ import app.softnetwork.payment.model.LegalUser.LegalUserType -import app.softnetwork.payment.model.PaymentUser.PaymentUserType +import app.softnetwork.payment.model.NaturalUser.NaturalUserType import app.softnetwork.payment.model._ import app.softnetwork.payment.spi._ import app.softnetwork.persistence._ import app.softnetwork.persistence.message.{BroadcastEvent, CrudEvent} import app.softnetwork.persistence.typed._ import app.softnetwork.scheduler.config.SchedulerSettings -import app.softnetwork.serialization.asJson -import app.softnetwork.time._ -import org.slf4j.Logger import app.softnetwork.scheduler.message.SchedulerEvents.{ ExternalEntityToSchedulerEvent, ExternalSchedulerEvent, @@ -29,6 +28,9 @@ import app.softnetwork.scheduler.message.SchedulerEvents.{ } import app.softnetwork.scheduler.message.{AddSchedule, RemoveSchedule} import app.softnetwork.scheduler.model.Schedule +import app.softnetwork.serialization.asJson +import app.softnetwork.time._ +import org.slf4j.Logger import java.time.LocalDate import java.util.Date @@ -37,14 +39,14 @@ import scala.util.{Failure, Success} /** Created by smanciot on 22/04/2022. */ -trait GenericPaymentBehavior +trait PaymentBehavior extends TimeStampedBehavior[ PaymentCommand, PaymentAccount, ExternalSchedulerEvent, PaymentResult ] - with ManifestWrapper[PaymentAccount] { _: PaymentProvider => + with ManifestWrapper[PaymentAccount] { override protected val manifestWrapper: ManifestW = ManifestW() @@ -53,7 +55,9 @@ trait GenericPaymentBehavior val nextRecurringPayment: String = "NextRecurringPayment" - def paymentDao: GenericPaymentDao + def paymentDao: PaymentDao = PaymentDao + + def softPayAccountDao: SoftPayAccountDao = SoftPayAccountDao /** @return * node role required to start this actor @@ -107,6 +111,8 @@ trait GenericPaymentBehavior ): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { implicit val system: ActorSystem[_] = context.system implicit val log: Logger = context.log + implicit val softPayClientSettings: SoftPayClientSettings = SoftPayClientSettings(system) + val internalClientId = Option(softPayClientSettings.clientId) command match { case cmd: CreateOrUpdatePaymentAccount => @@ -193,15 +199,22 @@ trait GenericPaymentBehavior case cmd: PreRegisterCard => import cmd._ var registerWallet: Boolean = false - loadPaymentAccount(entityId, state, PaymentAccount.User.NaturalUser(user)) match { + loadPaymentAccount(entityId, state, PaymentAccount.User.NaturalUser(user), clientId) match { case Some(paymentAccount) => + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ val lastUpdated = now() (paymentAccount.userId match { case None => createOrUpdatePaymentAccount( Some( paymentAccount.withNaturalUser( - user.withPaymentUserType(PaymentUserType.PAYER) + user.withNaturalUserType(NaturalUserType.PAYER) ) ) ) @@ -217,7 +230,7 @@ trait GenericPaymentBehavior }) match { case Some(walletId) => keyValueDao.addKeyValue(walletId, entityId) - val createOrUpdatePaymentAccount = + val paymentAccountUpsertedEvent = PaymentAccountUpsertedEvent.defaultInstance .withDocument( paymentAccount @@ -227,7 +240,7 @@ trait GenericPaymentBehavior user .withUserId(userId) .withWalletId(walletId) - .withPaymentUserType(PaymentUserType.PAYER) + .withNaturalUserType(NaturalUserType.PAYER) ) ) .withLastUpdated(lastUpdated) @@ -265,7 +278,7 @@ trait GenericPaymentBehavior .withLastName(user.lastName) .withBirthday(user.birthday) ) - ) ++ walletEvents :+ createOrUpdatePaymentAccount + ) ++ walletEvents :+ paymentAccountUpsertedEvent ) .thenRun(_ => CardPreRegistered(cardPreRegistration) ~> replyTo) case _ => @@ -279,13 +292,13 @@ trait GenericPaymentBehavior .withUserId(userId) .withWalletId(walletId) .withLastUpdated(lastUpdated) - ) :+ createOrUpdatePaymentAccount + ) :+ paymentAccountUpsertedEvent ) .thenRun(_ => CardNotPreRegistered ~> replyTo) } else { Effect .persist( - createOrUpdatePaymentAccount + paymentAccountUpsertedEvent ) .thenRun(_ => CardNotPreRegistered ~> replyTo) } @@ -298,7 +311,7 @@ trait GenericPaymentBehavior paymentAccount .copy( user = PaymentAccount.User.NaturalUser( - user.withUserId(userId).withPaymentUserType(PaymentUserType.PAYER) + user.withUserId(userId).withNaturalUserType(NaturalUserType.PAYER) ) ) .withLastUpdated(lastUpdated) @@ -314,7 +327,7 @@ trait GenericPaymentBehavior .withDocument( paymentAccount .withNaturalUser( - user.withPaymentUserType(PaymentUserType.PAYER) + user.withNaturalUserType(NaturalUserType.PAYER) ) .withLastUpdated(lastUpdated) ) @@ -329,6 +342,11 @@ trait GenericPaymentBehavior import cmd._ state match { case Some(paymentAccount) => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ paymentAccount.getNaturalUser.userId match { case Some(userId) => (registrationId match { @@ -380,6 +398,11 @@ trait GenericPaymentBehavior import cmd._ state match { case Some(paymentAccount) => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ loadCardPreAuthorized(orderUuid, preAuthorizationId) match { case Some(transaction) => handleCardPreAuthorization( @@ -400,13 +423,22 @@ trait GenericPaymentBehavior import cmd._ state match { case Some(paymentAccount) => + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ paymentAccount.transactions.find(_.id == cardPreAuthorizedTransactionId) match { case Some(preAuthorizationTransaction) => val preAuthorizationCanceled = cancelPreAuthorization(orderUuid, cardPreAuthorizedTransactionId) val updatedPaymentAccount = paymentAccount.withTransactions( paymentAccount.transactions.filterNot(_.id == cardPreAuthorizedTransactionId) :+ - preAuthorizationTransaction.withPreAuthorizationCanceled(preAuthorizationCanceled) + preAuthorizationTransaction + .withPreAuthorizationCanceled(preAuthorizationCanceled) + .copy(clientId = clientId) ) val lastUpdated = now() Effect @@ -434,6 +466,11 @@ trait GenericPaymentBehavior import cmd._ state match { case Some(paymentAccount) => + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) val maybeTransaction = paymentAccount.transactions.find(_.id == preAuthorizationId) maybeTransaction match { case None => @@ -486,10 +523,15 @@ trait GenericPaymentBehavior ) case Some(preAuthorizationTransaction) => // load credited payment account - paymentDao.loadPaymentAccount(creditedAccount) complete () match { + paymentDao.loadPaymentAccount(creditedAccount, clientId) complete () match { case Success(s) => s match { case Some(creditedPaymentAccount) => + val clientId = creditedPaymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ creditedPaymentAccount.walletId match { case Some(creditedWalletId) => payInWithCardPreAuthorized( @@ -557,6 +599,11 @@ trait GenericPaymentBehavior import cmd._ state match { case Some(paymentAccount) => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ paymentType match { case Transaction.PaymentType.CARD => paymentAccount.userId match { @@ -571,7 +618,7 @@ trait GenericPaymentBehavior }) match { case Some(cardId) => // load credited payment account - paymentDao.loadPaymentAccount(creditedAccount) complete () match { + paymentDao.loadPaymentAccount(creditedAccount, clientId) complete () match { case Success(s) => s match { case Some(creditedPaymentAccount) => @@ -647,7 +694,7 @@ trait GenericPaymentBehavior paymentAccount.userId match { case Some(userId) => // load credited payment account - paymentDao.loadPaymentAccount(creditedAccount) complete () match { + paymentDao.loadPaymentAccount(creditedAccount, clientId) complete () match { case Success(s) => s match { case Some(creditedPaymentAccount) => @@ -733,6 +780,11 @@ trait GenericPaymentBehavior import cmd._ state match { case Some(paymentAccount) => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ loadPayIn(orderUuid, transactionId, None) match { case Some(transaction) => handlePayIn( @@ -753,6 +805,11 @@ trait GenericPaymentBehavior import cmd._ state match { case Some(paymentAccount) => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ loadPayIn(orderUuid, transactionId, None) match { case Some(transaction) => handlePayIn( @@ -773,6 +830,13 @@ trait GenericPaymentBehavior import cmd._ state match { case Some(paymentAccount) => + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ (paymentAccount.transactions.find(_.id == payInTransactionId) match { case None => loadPayIn(orderUuid, payInTransactionId, None) case some => some @@ -805,10 +869,10 @@ trait GenericPaymentBehavior val updatedPaymentAccount = paymentAccount .withTransactions( paymentAccount.transactions.filterNot(_.id == transaction.id) - :+ transaction + :+ transaction.copy(clientId = clientId) ) .withLastUpdated(lastUpdated) - val upsertedEvent = + val paymentAccountUpsertedEvent = PaymentAccountUpsertedEvent.defaultInstance .withDocument(updatedPaymentAccount) .withLastUpdated(lastUpdated) @@ -827,7 +891,7 @@ trait GenericPaymentBehavior .withOrderUuid(orderUuid) .withResultMessage(transaction.resultMessage) .withTransaction(transaction) - ) :+ upsertedEvent + ) :+ paymentAccountUpsertedEvent ) .thenRun(_ => RefundFailed( @@ -861,7 +925,7 @@ trait GenericPaymentBehavior .withReasonMessage(reasonMessage) .withInitializedByClient(initializedByClient) .withPaymentType(transaction.paymentType) - ) :+ upsertedEvent + ) :+ paymentAccountUpsertedEvent ) .thenRun(_ => Refunded(transaction.id, transaction.status) ~> replyTo) } else { @@ -878,7 +942,7 @@ trait GenericPaymentBehavior .withOrderUuid(orderUuid) .withResultMessage(transaction.resultMessage) .withTransaction(transaction) - ) :+ upsertedEvent + ) :+ paymentAccountUpsertedEvent ) .thenRun(_ => RefundFailed( @@ -920,6 +984,13 @@ trait GenericPaymentBehavior import cmd._ state match { case Some(paymentAccount) => + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ paymentAccount.userId match { case Some(userId) => paymentAccount.walletId match { @@ -946,7 +1017,7 @@ trait GenericPaymentBehavior val updatedPaymentAccount = paymentAccount .withTransactions( paymentAccount.transactions.filterNot(_.id == transaction.id) - :+ transaction + :+ transaction.copy(clientId = clientId) ) .withLastUpdated(lastUpdated) if (transaction.status.isTransactionFailedForTechnicalReason) { @@ -1117,13 +1188,20 @@ trait GenericPaymentBehavior import cmd._ state match { case Some(paymentAccount) => // debited account + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ var maybeCreditedPaymentAccount: Option[PaymentAccount] = None transfer(paymentAccount.userId match { case Some(authorId) => paymentAccount.walletId match { case Some(debitedWalletId) => // load credited payment account - paymentDao.loadPaymentAccount(creditedAccount) complete () match { + paymentDao.loadPaymentAccount(creditedAccount, clientId) complete () match { case Success(s) => maybeCreditedPaymentAccount = s maybeCreditedPaymentAccount match { @@ -1164,7 +1242,7 @@ trait GenericPaymentBehavior val updatedPaymentAccount = paymentAccount .withTransactions( paymentAccount.transactions.filterNot(_.id == transaction.id) - :+ transaction + :+ transaction.copy(clientId = clientId) ) .withLastUpdated(lastUpdated) if ( @@ -1269,7 +1347,8 @@ trait GenericPaymentBehavior creditedAccount, paymentAccount, creditedUserId, - bankAccountId + bankAccountId, + clientId ) } case _ => Effect.none.thenRun(_ => BankAccountNotFound ~> replyTo) @@ -1279,7 +1358,7 @@ trait GenericPaymentBehavior case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) } - case _: CancelMandate => + case cmd: CancelMandate => state match { case Some(paymentAccount) => if (paymentAccount.mandateExists && paymentAccount.mandateRequired) { @@ -1291,6 +1370,13 @@ trait GenericPaymentBehavior ) .thenRun(_ => MandateNotCanceled ~> replyTo) } else { + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ paymentAccount.bankAccount match { case Some(bankAccount) => bankAccount.mandateId match { @@ -1345,6 +1431,11 @@ trait GenericPaymentBehavior case Some(userId) => paymentAccount.bankAccount.flatMap(_.id) match { case Some(bankAccountId) => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ loadMandate(Some(mandateId), userId, bankAccountId) match { case Some(report) => val internalStatus = @@ -1399,6 +1490,13 @@ trait GenericPaymentBehavior paymentAccount.bankAccount.flatMap(_.mandateId) match { case Some(mandateId) => if (paymentAccount.mandateActivated) { + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ directDebit( Some( DirectDebitTransaction.defaultInstance @@ -1419,7 +1517,7 @@ trait GenericPaymentBehavior val updatedPaymentAccount = paymentAccount .withTransactions( paymentAccount.transactions.filterNot(_.id == transaction.id) - :+ transaction + :+ transaction.copy(clientId = clientId) ) .withLastUpdated(lastUpdated) if ( @@ -1489,6 +1587,13 @@ trait GenericPaymentBehavior case Some(transaction) => paymentAccount.walletId match { case Some(creditedWalletId) => + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ val transactionDate: LocalDate = Date.from(transaction.createdDate) directDebitTransaction( transaction.creditedWalletId.getOrElse(creditedWalletId), @@ -1505,7 +1610,7 @@ trait GenericPaymentBehavior val updatedPaymentAccount = paymentAccount .withTransactions( paymentAccount.transactions.filterNot(_.id == t.id) - :+ updatedTransaction + :+ updatedTransaction.copy(clientId = clientId) ) .withLastUpdated(lastUpdated) if (t.status.isTransactionSucceeded || t.status.isTransactionCreated) { @@ -1567,6 +1672,11 @@ trait GenericPaymentBehavior _.getId == cmd.recurringPayInRegistrationId ) match { case Some(recurringPayment) => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ createRecurringCardPayment( RecurringPaymentTransaction.defaultInstance .withExternalUuid(paymentAccount.externalUuid) @@ -1612,6 +1722,11 @@ trait GenericPaymentBehavior import cmd._ paymentAccount.recurryingPayments.find(_.getId == recurringPayInRegistrationId) match { case Some(recurringPayment) => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ loadPayIn("", transactionId, Some(recurringPayInRegistrationId)) match { case Some(transaction) => handleRecurringPayment( @@ -1663,6 +1778,11 @@ trait GenericPaymentBehavior _.getId == recurringPaymentRegistrationId ) match { case Some(recurringPayment) => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ val debitedAmount = nextDebitedAmount.getOrElse( recurringPayment.nextDebitedAmount.getOrElse( recurringPayment.firstDebitedAmount @@ -1684,7 +1804,11 @@ trait GenericPaymentBehavior .withDebitedAmount(debitedAmount) .withFeesAmount(feesAmount) .withCurrency(currency) - .withStatementDescriptor(statementDescriptor.getOrElse("")) // TODO + .withStatementDescriptor( + statementDescriptor + .orElse(recurringPayment.statementDescriptor) + .getOrElse("") + ) // TODO ) match { case Some(transaction) => handleRecurringPayment( @@ -1855,7 +1979,7 @@ trait GenericPaymentBehavior case None => cmd.user match { case Some(user) => - loadPaymentAccount(entityId, None, user) + loadPaymentAccount(entityId, None, user, cmd.clientId) case _ => None } case some => some @@ -1877,9 +2001,9 @@ trait GenericPaymentBehavior userId = previousLegalUser.legalRepresentative.userId, walletId = previousLegalUser.legalRepresentative.walletId ) - .withPaymentUserType( - updatedLegalUser.legalRepresentative.paymentUserType - .getOrElse(PaymentUserType.COLLECTOR) + .withNaturalUserType( + updatedLegalUser.legalRepresentative.naturalUserType + .getOrElse(NaturalUserType.COLLECTOR) ), uboDeclaration = previousLegalUser.uboDeclaration, lastAcceptedTermsOfPSP = previousLegalUser.lastAcceptedTermsOfPSP @@ -1890,9 +2014,9 @@ trait GenericPaymentBehavior PaymentAccount.User.LegalUser( updatedLegalUser.copy( legalRepresentative = - updatedLegalUser.legalRepresentative.withPaymentUserType( - updatedLegalUser.legalRepresentative.paymentUserType.getOrElse( - PaymentUserType.COLLECTOR + updatedLegalUser.legalRepresentative.withNaturalUserType( + updatedLegalUser.legalRepresentative.naturalUserType.getOrElse( + NaturalUserType.COLLECTOR ) ) ) @@ -1906,15 +2030,15 @@ trait GenericPaymentBehavior userId = previousNaturalUser.userId, walletId = previousNaturalUser.walletId ) - .withPaymentUserType( - updatedNaturalUser.paymentUserType.getOrElse(PaymentUserType.COLLECTOR) + .withNaturalUserType( + updatedNaturalUser.naturalUserType.getOrElse(NaturalUserType.COLLECTOR) ) ) } else if (updatedUser.isNaturalUser) { val updatedNaturalUser = updatedUser.naturalUser.get PaymentAccount.User.NaturalUser( - updatedNaturalUser.withPaymentUserType( - updatedNaturalUser.paymentUserType.getOrElse(PaymentUserType.COLLECTOR) + updatedNaturalUser.withNaturalUserType( + updatedNaturalUser.naturalUserType.getOrElse(NaturalUserType.COLLECTOR) ) ) } else { @@ -2057,6 +2181,11 @@ trait GenericPaymentBehavior updatedPaymentAccount.getLegalUser.uboDeclarationRequired && updatedPaymentAccount.getLegalUser.uboDeclaration.isEmpty + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ (paymentAccount.userId match { case None => createOrUpdatePaymentAccount(Some(updatedPaymentAccount)) @@ -2288,6 +2417,11 @@ trait GenericPaymentBehavior import cmd._ state match { case Some(paymentAccount) if paymentAccount.hasAcceptedTermsOfPSP => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ paymentAccount.userId match { case Some(userId) => addDocument(userId, entityId, pages, kycDocumentType) match { @@ -2412,6 +2546,11 @@ trait GenericPaymentBehavior case cmd: CreateOrUpdateUbo => state match { case Some(paymentAccount) => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ var declarationCreated: Boolean = false def createInternalDeclaration(): Option[UboDeclaration] = { createDeclaration(paymentAccount.userId.getOrElse("")) match { @@ -2486,6 +2625,11 @@ trait GenericPaymentBehavior case Some(uboDeclaration) if uboDeclaration.status.isUboDeclarationCreated || uboDeclaration.status.isUboDeclarationIncomplete || uboDeclaration.status.isUboDeclarationRefused => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ validateDeclaration(paymentAccount.userId.getOrElse(""), uboDeclaration.id) match { case Some(declaration) => val updatedUbo = declaration.withUbos(uboDeclaration.ubos) @@ -2536,6 +2680,11 @@ trait GenericPaymentBehavior if cmd.uboDeclarationId == uboDeclaration.id && uboDeclaration.status.isUboDeclarationValidationAsked => import cmd._ + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ getDeclaration(paymentAccount.userId.getOrElse(""), uboDeclarationId) match { case Some(declaration) => val internalStatus = { @@ -2839,6 +2988,11 @@ trait GenericPaymentBehavior case Some(_) => Effect.none.thenRun(_ => CardNotDisabled ~> replyTo) case _ => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ disableCard(cmd.cardId) match { case Some(_) => val lastUpdated = now() @@ -2896,6 +3050,13 @@ trait GenericPaymentBehavior nextDebitedAmount = cmd.nextDebitedAmount, nextFeesAmount = cmd.nextFeesAmount ) + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ registerRecurringCardPayment( userId, walletId, @@ -3024,6 +3185,11 @@ trait GenericPaymentBehavior _.getId == cmd.recurringPayInRegistrationId ) match { case Some(recurringPayment) if recurringPayment.`type`.isCard => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ val cardId: Option[String] = cmd.cardId match { case Some(cardId) => @@ -3189,9 +3355,17 @@ trait GenericPaymentBehavior creditedAccount: String, paymentAccount: PaymentAccount, creditedUserId: String, - bankAccountId: String - )(implicit context: ActorContext[_]): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { + bankAccountId: String, + clientId: Option[String] = None + )(implicit + context: ActorContext[_], + softPayClientSettings: SoftPayClientSettings + ): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { implicit val system: ActorSystem[_] = context.system + val _clientId = + paymentAccount.clientId.orElse(clientId).orElse(Option(softPayClientSettings.clientId)) + val paymentProvider = loadPaymentProvider(_clientId) + import paymentProvider._ mandate(creditedAccount, creditedUserId, bankAccountId) match { case Some(mandateResult) => if (mandateResult.status.isMandateFailed) { @@ -3254,9 +3428,14 @@ trait GenericPaymentBehavior private[this] def loadPaymentAccount( entityId: String, state: Option[PaymentAccount], - user: PaymentAccount.User - )(implicit system: ActorSystem[_], log: Logger): Option[PaymentAccount] = { - val pa = PaymentAccount.defaultInstance.withUser(user) + user: PaymentAccount.User, + clientId: Option[String] + )(implicit + system: ActorSystem[_], + log: Logger, + softPayClientSettings: SoftPayClientSettings + ): Option[PaymentAccount] = { + val pa = PaymentAccount.defaultInstance.withUser(user).copy(clientId = clientId) val uuid = pa.externalUuidWithProfile state match { case None => @@ -3284,7 +3463,13 @@ trait GenericPaymentBehavior None } else { keyValueDao.addKeyValue(uuid, entityId) - Some(paymentAccount) + Some( + paymentAccount.copy(clientId = + paymentAccount.clientId + .orElse(clientId) + .orElse(Option(softPayClientSettings.clientId)) + ) + ) } } } @@ -3525,7 +3710,8 @@ trait GenericPaymentBehavior transaction: Transaction )(implicit system: ActorSystem[_], - log: Logger + log: Logger, + softPayClientSettings: SoftPayClientSettings ): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { keyValueDao.addKeyValue( transaction.id, @@ -3552,6 +3738,11 @@ trait GenericPaymentBehavior case _ => if (transaction.status.isTransactionSucceeded || transaction.status.isTransactionCreated) { log.debug("Order-{} paid in: {} -> {}", orderUuid, transaction.id, asJson(transaction)) + val clientId = paymentAccount.clientId.orElse( + Option(softPayClientSettings.clientId) + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ val registerCardEvents: List[ExternalSchedulerEvent] = if (registerCard) { transaction.cardId match { @@ -3592,8 +3783,10 @@ trait GenericPaymentBehavior val updatedTransaction = updatedPaymentAccount.transactions .find(_.id == preAuthorizationId) .map( - _.copy(preAuthorizationValidated = - Some(validatePreAuthorization(transaction.orderUuid, preAuthorizationId)) + _.copy( + preAuthorizationValidated = + Some(validatePreAuthorization(transaction.orderUuid, preAuthorizationId)), + clientId = clientId ) ) updatedPaymentAccount.withTransactions( @@ -3660,7 +3853,8 @@ trait GenericPaymentBehavior transaction: Transaction )(implicit system: ActorSystem[_], - log: Logger + log: Logger, + softPayClientSettings: SoftPayClientSettings ): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { keyValueDao.addKeyValue( transaction.id, @@ -3672,6 +3866,7 @@ trait GenericPaymentBehavior .withTransactions( paymentAccount.transactions .filterNot(_.id == transaction.id) :+ transaction + .copy(clientId = paymentAccount.clientId) ) .withLastUpdated(lastUpdated) transaction.status match { @@ -3696,6 +3891,9 @@ trait GenericPaymentBehavior transaction.id, asJson(transaction) ) + val clientId = paymentAccount.clientId.orElse(Option(softPayClientSettings.clientId)) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ val registerCardEvents: List[ExternalSchedulerEvent] = if (registerCard) { transaction.cardId match { @@ -3775,13 +3973,17 @@ trait GenericPaymentBehavior documentId: String, maybeStatus: Option[KycDocument.KycDocumentStatus] = None )(implicit - system: ActorSystem[_] + system: ActorSystem[_], + softPayClientSettings: SoftPayClientSettings ): (KycDocumentValidationReport, List[ExternalSchedulerEvent]) = { var events: List[ExternalSchedulerEvent] = List.empty val lastUpdated = now() val userId = paymentAccount.userId.getOrElse("") + val clientId = paymentAccount.clientId.orElse(Option(softPayClientSettings.clientId)) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ val report = loadDocumentStatus(userId, documentId) val internalStatus = @@ -3943,4 +4145,21 @@ trait GenericPaymentBehavior newDocuments } + + @InternalApi + private[this] def loadPaymentProvider( + clientId: Option[String] + )(implicit system: ActorSystem[_]): PaymentProvider = { + PaymentProviders.paymentProvider( + clientId + .flatMap(softPayAccountDao.loadProvider(_) complete () match { + case Success(s) => s + case Failure(_) => None + }) + .getOrElse(throw new Exception("Payment provider not found")) + ) + } + } + +object PaymentBehavior extends PaymentBehavior diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/typed/SoftPayAccountBehavior.scala b/core/src/main/scala/app/softnetwork/payment/persistence/typed/SoftPayAccountBehavior.scala new file mode 100644 index 0000000..07c9433 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/persistence/typed/SoftPayAccountBehavior.scala @@ -0,0 +1,464 @@ +package app.softnetwork.payment.persistence.typed + +import akka.actor.typed.{ActorRef, ActorSystem} +import akka.actor.typed.scaladsl.{ActorContext, TimerScheduler} +import akka.persistence.typed.scaladsl.Effect +import app.softnetwork.account.config.AccountSettings.{ActivationTokenExpirationTime, BaseUrl, Path} +import app.softnetwork.account.handlers.{DefaultGenerator, Generator} +import app.softnetwork.account.message._ +import app.softnetwork.account.model.{BasicAccount, BasicAccountProfile, Principal, PrincipalType} +import app.softnetwork.account.persistence.typed.AccountBehavior +import app.softnetwork.notification.message.ExternalEntityToNotificationEvent +import app.softnetwork.payment.cli.Main +import app.softnetwork.payment.message.AccountMessages +import app.softnetwork.payment.message.AccountMessages.SoftPaySignUp +import app.softnetwork.payment.message.SoftPayAccountEvents.{ + SoftPayAccountCreatedEvent, + SoftPayAccountProviderRegisteredEvent, + SoftPayAccountTokenRefreshedEvent, + SoftPayAccountTokenRegisteredEvent +} +import app.softnetwork.payment.model.SoftPayAccount +import app.softnetwork.payment.spi.PaymentProviders +import app.softnetwork.persistence.typed._ +import app.softnetwork.scheduler.message.SchedulerEvents.ExternalSchedulerEvent +import app.softnetwork.security.sha256 +import mustache.Mustache +import org.slf4j.Logger + +import java.time.Instant + +trait SoftPayAccountBehavior extends AccountBehavior[SoftPayAccount, BasicAccountProfile] { + _: Generator => + override protected def createAccount( + entityId: String, + cmd: SignUp + )(implicit context: ActorContext[AccountCommand]): Option[SoftPayAccount] = { + cmd match { + case SoftPaySignUp(_, _, provider, _, _) => + PaymentProviders.paymentProvider(provider).client match { + case Some(client) => + SoftPayAccount(BasicAccount(cmd, Some(entityId))) + .map(account => + account + .withClients(Seq(client.withClientApiKey(client.generateApiKey()))) + .withSecondaryPrincipals( + account.secondaryPrincipals.filterNot( + _.`type` == PrincipalType.Other + ) :+ Principal(PrincipalType.Other, client.clientId) + ) + ) + case _ => None + } + case _ => SoftPayAccount(BasicAccount(cmd, Some(entityId))) + } + } + + override protected def createProfileUpdatedEvent( + uuid: String, + profile: BasicAccountProfile, + loginUpdated: Option[Boolean] + )(implicit context: ActorContext[AccountCommand]): ProfileUpdatedEvent[BasicAccountProfile] = + BasicAccountProfileUpdatedEvent(uuid, profile, loginUpdated) + + override protected def createAccountCreatedEvent( + account: SoftPayAccount + )(implicit context: ActorContext[AccountCommand]): AccountCreatedEvent[SoftPayAccount] = + SoftPayAccountCreatedEvent(account) + + /** @param entityId + * - entity identity + * @param state + * - current state + * @param command + * - command to handle + * @param replyTo + * - optional actor to reply to + * @return + * effect + */ + override def handleCommand( + entityId: String, + state: Option[SoftPayAccount], + command: AccountCommand, + replyTo: Option[ActorRef[AccountCommandResult]], + timers: TimerScheduler[AccountCommand] + )(implicit + context: ActorContext[AccountCommand] + ): Effect[ExternalSchedulerEvent, Option[SoftPayAccount]] = { + implicit val system: ActorSystem[_] = context.system + command match { + case AccountMessages.RegisterClientWithProvider(provider) => + state match { + case Some(account) => + if ( + account.clients.exists(cl => + cl.provider.providerId == provider.providerId && + cl.provider.providerApiKey == provider.providerApiKey + ) + ) { + Effect.none.thenRun { _ => + AccountMessages.ProviderAlreadyRegistered ~> replyTo + } + } else if ( + (accountKeyDao.lookupAccount(provider.clientId) complete ()).exists(_ != entityId) + ) { + Effect.none.thenRun { _ => + AccountMessages.ProviderAlreadyRegistered ~> replyTo + } + } else { + PaymentProviders.paymentProvider(provider).client match { + case Some(client) => + val updatedClient = + account.clients + .find(_.provider.providerId == provider.providerId) + .map(_.withProvider(provider)) + .getOrElse(client.withClientApiKey(client.generateApiKey())) + accountKeyDao.addAccountKey(updatedClient.clientId, entityId) + Effect + .persist( + SoftPayAccountProviderRegisteredEvent( + updatedClient, + Instant.now() + ) + ) + .thenRun(_ => + AccountMessages.ClientWithProviderRegistered(updatedClient) ~> replyTo + ) + case _ => + Effect.none.thenRun { _ => + AccountMessages.ClientWithProviderNotRegistered ~> replyTo + } + } + } + case _ => + Effect.none.thenRun { _ => + AccountNotFound ~> replyTo + } + } + + case AccountMessages.LoadClient(clientId) => + state match { + case Some(account) => + account.clients.find(_.clientId == clientId) match { + case Some(client) => + Effect.none.thenRun { _ => + AccountMessages.ClientLoaded(client) ~> replyTo + } + case _ => + Effect.none.thenRun { _ => + AccountMessages.ClientNotFound ~> replyTo + } + } + case _ => + Effect.none.thenRun { _ => + AccountNotFound ~> replyTo + } + } + + case AccountMessages.ListApiKeys => + state match { + case Some(account) => + Effect.none.thenRun { _ => + AccountMessages.ApiKeysLoaded(account.apiKeys) ~> replyTo + } + case _ => + Effect.none.thenRun { _ => + AccountNotFound ~> replyTo + } + } + + case AccountMessages.LoadApiKey(clientId) => + state match { + case Some(account) => + account.apiKeys.find(_.clientId == clientId) match { + case Some(apiKey) => + Effect.none.thenRun { _ => + AccountMessages.ApiKeyLoaded(apiKey) ~> replyTo + } + case _ => + Effect.none.thenRun { _ => + AccountMessages.ApiKeyNotFound ~> replyTo + } + } + case _ => + Effect.none.thenRun { _ => + AccountNotFound ~> replyTo + } + } + + case AccountMessages.GenerateClientToken( + clientId, + clientSecret, + scope + ) => // grant_type=client_credentials + state match { + case Some(account) if account.status.isActive => + account.clients.find(_.clientId == clientId) match { + case Some(client) if client.accessToken.exists(!_.expired) => + Effect.none.thenRun(_ => AccessTokenAlreadyExists ~> replyTo) + + case Some(client) if client.clientApiKey.isEmpty => + Effect.none.thenRun(_ => AccountMessages.ClientNotFound ~> replyTo) + + case Some(client) => + if (client.getClientApiKey == clientSecret) { + val accessToken = + generator.generateAccessToken( + account.primaryPrincipal.value, + scope + ) + accountKeyDao.addAccountKey(accessToken.token, entityId) + accountKeyDao.addAccountKey(accessToken.refreshToken, entityId) + Effect + .persist( + SoftPayAccountTokenRegisteredEvent( + client.withAccessToken( + accessToken.copy( + token = sha256(accessToken.token), + refreshToken = sha256(accessToken.refreshToken) + ) + ), + Instant.now() + ) + ) + .thenRun { _ => + AccessTokenGenerated(accessToken) ~> replyTo + } + } else { + Effect.none.thenRun { _ => + AccountMessages.ClientNotFound ~> replyTo + } + } + + case _ => + Effect.none.thenRun { _ => + AccountMessages.ClientNotFound ~> replyTo + } + } + + case Some(account) if !account.status.isActive => + inactiveAccount(entityId, account, replyTo) + + case Some(account) if account.status.isDisabled => + Effect.none.thenRun(_ => AccountDisabled ~> replyTo) + + case Some(account) if account.status.isDeleted => + Effect.none.thenRun(_ => AccountDeleted(account) ~> replyTo) + + case _ => + Effect.none.thenRun { _ => + AccountNotFound ~> replyTo + } + } + + case AccountMessages.RefreshClientToken(refreshToken) => + state match { + case Some(account) if account.status.isActive => + account.clients.find( + _.accessToken.exists(at => + at.refreshToken == sha256(refreshToken) && !at.refreshExpired + ) + ) match { + case Some(client) => + val previousToken = client.getAccessToken + val accessToken = + generator.generateAccessToken( + account.primaryPrincipal.value, + previousToken.scope + ) + accountKeyDao.removeAccountKey(previousToken.token) + accountKeyDao.removeAccountKey(previousToken.refreshToken) + accountKeyDao.addAccountKey(accessToken.token, entityId) + accountKeyDao.addAccountKey(accessToken.refreshToken, entityId) + Effect + .persist( + SoftPayAccountTokenRefreshedEvent( + client.withAccessToken( + accessToken.copy( + token = sha256(accessToken.token), + refreshToken = sha256(accessToken.refreshToken) + ) + ), + Instant.now() + ) + ) + .thenRun { _ => + AccessTokenRefreshed(accessToken) ~> replyTo + } + + case _ => + Effect.none.thenRun { _ => + AccountMessages.ClientNotFound ~> replyTo + } + } + + case Some(account) if !account.status.isActive => + inactiveAccount(entityId, account, replyTo) + + case Some(account) if account.status.isDisabled => + Effect.none.thenRun(_ => AccountDisabled ~> replyTo) + + case Some(account) if account.status.isDeleted => + Effect.none.thenRun(_ => AccountDeleted(account) ~> replyTo) + + case _ => + Effect.none.thenRun { _ => + AccountNotFound ~> replyTo + } + } + + case AccountMessages.OAuthClient(token) => + state match { + case Some(account) if account.status.isActive => + account.clients.find( + _.accessToken.map(_.token).getOrElse("") == sha256(token) + ) match { + case Some(client) if client.getAccessToken.expired => + Effect.none.thenRun(_ => TokenExpired ~> replyTo) + + case Some(client) => + Effect.none.thenRun { _ => + AccountMessages.OAuthClientSucceededResult(client) ~> replyTo + } + + case _ => + Effect.none.thenRun { _ => + AccountMessages.ClientNotFound ~> replyTo + } + } + + case Some(account) if account.status.isDisabled => + Effect.none.thenRun(_ => AccountDisabled ~> replyTo) + + case Some(account) if account.status.isDeleted => + Effect.none.thenRun(_ => AccountDeleted(account) ~> replyTo) + + case _ => + Effect.none.thenRun { _ => + AccountNotFound ~> replyTo + } + } + + case AccountMessages.RegisterAccountWithProvider(provider) => + state match { + case Some(account) => + val updatedClient = account.clients.find(_.clientId == provider.clientId) match { + case Some(client) => + client.withClientApiKey(provider.client.getClientApiKey) + case _ => + provider.client + } + accountKeyDao.addAccountKey(provider.clientId, entityId) + Effect + .persist( + List( + AccountActivatedEvent(entityId, Some(Instant.now())), + SoftPayAccountProviderRegisteredEvent( + updatedClient, + Instant.now() + ) + ) + ) + .thenRun(state => + AccountMessages.AccountWithProviderRegistered(state.getOrElse(account)) ~> replyTo + ) + case _ => + val account = provider.account + accountKeyDao.addAccountKey(provider.clientId, entityId) + Effect.persist(SoftPayAccountCreatedEvent(account)).thenRun { state => + AccountMessages.AccountWithProviderRegistered(state.getOrElse(account)) ~> replyTo + } + } + + case _ => super.handleCommand(entityId, state, command, replyTo, timers) + } + } + + /** @param state + * - current state + * @param event + * - event to hanlde + * @return + * new state + */ + override def handleEvent( + state: Option[SoftPayAccount], + event: ExternalSchedulerEvent + )(implicit context: ActorContext[_]): Option[SoftPayAccount] = { + event match { + case SoftPayAccountProviderRegisteredEvent(client, lastUpdated) => + state.map(account => { + account + .withClients(account.clients.filterNot(_.clientId == client.clientId) :+ client) + .withLastUpdated(lastUpdated) + }) + + case SoftPayAccountTokenRegisteredEvent(client, lastUpdated) => + state.map(account => { + account + .withClients(account.clients.filterNot(_.clientId == client.clientId) :+ client) + .withLastUpdated(lastUpdated) + }) + + case SoftPayAccountTokenRefreshedEvent(client, lastUpdated) => + state.map(account => { + account + .withClients(account.clients.filterNot(_.clientId == client.clientId) :+ client) + .withLastUpdated(lastUpdated) + }) + + case _ => super.handleEvent(state, event) + } + } + + private def inactiveAccount( + entityId: String, + account: SoftPayAccount, + replyTo: Option[ActorRef[AccountCommandResult]] + )(implicit + context: ActorContext[_] + ): Effect[ExternalEntityToNotificationEvent, Option[SoftPayAccount]] = { + implicit val log: Logger = context.log + implicit val system: ActorSystem[Nothing] = context.system + def help(token: String): String = { + Mustache("snippets/account/inactive.mustache").render( + Map( + "command" -> s"${Main.shell} activate -t $token", + "activationUrl" -> s"$BaseUrl/$Path/activate?token=$token" + ) + ) + } + account.verificationToken match { + case Some(v) => + if (v.expired) { + accountKeyDao.removeAccountKey(v.token) + val activationToken = generator.generateToken( + account.primaryPrincipal.value, + ActivationTokenExpirationTime + ) + accountKeyDao.addAccountKey(activationToken.token, entityId) + val notifications = sendActivation(entityId, account, activationToken) + Effect + .persist(notifications.toList) + .thenRun(_ => AccountMessages.InactiveAccount(help(activationToken.token)) ~> replyTo) + } else { + Effect.none.thenRun(_ => AccountMessages.InactiveAccount(help(v.token)) ~> replyTo) + } + case _ => + val activationToken = generator.generateToken( + account.primaryPrincipal.value, + ActivationTokenExpirationTime + ) + accountKeyDao.addAccountKey(activationToken.token, entityId) + val notifications = sendActivation(entityId, account, activationToken) + Effect + .persist(notifications.toList) + .thenRun(_ => AccountMessages.InactiveAccount(help(activationToken.token)) ~> replyTo) + } + } +} + +case object SoftPayAccountBehavior extends SoftPayAccountBehavior with DefaultGenerator { + override def persistenceId: String = "SoftPayAccount" +} 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 c7317d3..e14650a 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/BankAccountEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/BankAccountEndpoints.scala @@ -1,9 +1,10 @@ package app.softnetwork.payment.service import app.softnetwork.payment.config.PaymentSettings -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages._ import app.softnetwork.payment.model.{BankAccountView, PaymentAccount} +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} import sttp.capabilities import sttp.capabilities.akka.AkkaStreams import sttp.model.StatusCode @@ -12,72 +13,76 @@ import sttp.tapir.server.ServerEndpoint import scala.concurrent.Future -trait BankAccountEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler => +trait BankAccountEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { + _: RootPaymentEndpoints[SD] with PaymentHandler => import app.softnetwork.serialization._ val createOrUpdateBankAccount: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.post + requiredSessionEndpoint.post .in(PaymentSettings.BankRoute) .in(jsonBody[BankAccountCommand].description("Legal or natural user bank account")) .out( statusCode(StatusCode.Ok) .and(jsonBody[BankAccountCreatedOrUpdated].description("Bank account created or updated")) ) - .serverLogic(session => { bank => - import bank._ - var externalUuid: String = "" - val updatedUser: Option[PaymentAccount.User] = { - user match { - case Left(naturalUser) => - var updatedNaturalUser = { - if (naturalUser.externalUuid.trim.isEmpty) { - naturalUser.withExternalUuid(session.id) - } else { - naturalUser + .serverLogic { case (client, session) => + bank => + import bank._ + var externalUuid: String = "" + val updatedUser: Option[PaymentAccount.User] = { + user match { + case Left(naturalUser) => + var updatedNaturalUser = { + if (naturalUser.externalUuid.trim.isEmpty) { + naturalUser.withExternalUuid(session.id) + } else { + naturalUser + } } - } - session.profile match { - case Some(profile) if updatedNaturalUser.profile.isEmpty => - updatedNaturalUser = updatedNaturalUser.withProfile(profile) - case _ => - } - externalUuid = updatedNaturalUser.externalUuid - Some(PaymentAccount.User.NaturalUser(updatedNaturalUser)) - case Right(legalUser) => - var updatedLegalRepresentative = legalUser.legalRepresentative - if (updatedLegalRepresentative.externalUuid.trim.isEmpty) { - updatedLegalRepresentative = updatedLegalRepresentative.withExternalUuid(session.id) - } - session.profile match { - case Some(profile) if updatedLegalRepresentative.profile.isEmpty => - updatedLegalRepresentative = updatedLegalRepresentative.withProfile(profile) - case _ => - } - externalUuid = updatedLegalRepresentative.externalUuid - Some( - PaymentAccount.User.LegalUser( - legalUser.withLegalRepresentative(updatedLegalRepresentative) + session.profile match { + case Some(profile) if updatedNaturalUser.profile.isEmpty => + updatedNaturalUser = updatedNaturalUser.withProfile(profile) + case _ => + } + externalUuid = updatedNaturalUser.externalUuid + Some(PaymentAccount.User.NaturalUser(updatedNaturalUser)) + case Right(legalUser) => + var updatedLegalRepresentative = legalUser.legalRepresentative + if (updatedLegalRepresentative.externalUuid.trim.isEmpty) { + updatedLegalRepresentative = + updatedLegalRepresentative.withExternalUuid(session.id) + } + session.profile match { + case Some(profile) if updatedLegalRepresentative.profile.isEmpty => + updatedLegalRepresentative = updatedLegalRepresentative.withProfile(profile) + case _ => + } + externalUuid = updatedLegalRepresentative.externalUuid + Some( + PaymentAccount.User.LegalUser( + legalUser.withLegalRepresentative(updatedLegalRepresentative) + ) ) - ) + } } - } - run( - CreateOrUpdateBankAccount( - externalUuidWithProfile(session), - bankAccount.withExternalUuid(externalUuid), - updatedUser, - acceptedTermsOfPSP - ) - ).map { - case r: BankAccountCreatedOrUpdated => Right(r) - case other => Left(error(other)) - } - }) + run( + CreateOrUpdateBankAccount( + externalUuidWithProfile(session), + bankAccount.withExternalUuid(externalUuid), + updatedUser, + acceptedTermsOfPSP, + clientId = client.map(_.clientId).orElse(session.clientId) + ) + ).map { + case r: BankAccountCreatedOrUpdated => Right(r) + case other => Left(error(other)) + } + } .description("Create or update legal or natural user bank account") val loadBankAccount: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.get + requiredSessionEndpoint.get .in(PaymentSettings.BankRoute) .out( statusCode(StatusCode.Ok).and( @@ -85,27 +90,30 @@ trait BankAccountEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler .description("Authenticated user bank account") ) ) - .serverLogic(session => + .serverLogic { case (client, session) => _ => { run( - LoadBankAccount(externalUuidWithProfile(session)) + LoadBankAccount( + externalUuidWithProfile(session), + clientId = client.map(_.clientId).orElse(session.clientId) + ) ).map { case r: BankAccountLoaded => Right(r.bankAccount.view) case other => Left(error(other)) } } - ) + } .description("Load authenticated user bank account") val deleteBankAccount: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.delete + requiredSessionEndpoint.delete .in(PaymentSettings.BankRoute) .out( statusCode(StatusCode.Ok).and(jsonBody[BankAccountDeleted.type]) ) - .serverLogic(session => + .serverLogic(principal => _ => - run(DeleteBankAccount(externalUuidWithProfile(session), Some(false))).map { + run(DeleteBankAccount(externalUuidWithProfile(principal._2), Some(false))).map { case BankAccountDeleted => Right(BankAccountDeleted) case other => Left(error(other)) } diff --git a/core/src/main/scala/app/softnetwork/payment/service/BasicPaymentService.scala b/core/src/main/scala/app/softnetwork/payment/service/BasicPaymentService.scala index 6d9a7e3..47e5876 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/BasicPaymentService.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/BasicPaymentService.scala @@ -1,18 +1,17 @@ package app.softnetwork.payment.service import app.softnetwork.api.server.ApiErrors -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages._ -import app.softnetwork.payment.model.{computeExternalUuidWithProfile, BrowserInfo} +import app.softnetwork.payment.model.BrowserInfo import app.softnetwork.persistence.service.Service -import org.softnetwork.session.model.Session import java.util.TimeZone import scala.concurrent.Future import scala.reflect.ClassTag trait BasicPaymentService extends Service[PaymentCommand, PaymentResult] { - _: GenericPaymentHandler => + _: PaymentHandler => def run(command: PaymentCommandWithKey)(implicit tTag: ClassTag[PaymentCommand] @@ -37,9 +36,6 @@ trait BasicPaymentService extends Service[PaymentCommand, PaymentResult] { case _ => ApiErrors.BadRequest("Unknown") } - protected[payment] def externalUuidWithProfile(session: Session): String = - computeExternalUuidWithProfile(session.id, session.profile) - protected[payment] def extractBrowserInfo( language: Option[String], accept: Option[String], diff --git a/core/src/main/scala/app/softnetwork/payment/service/CardEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/CardEndpoints.scala index 0d112f0..86b94a1 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/CardEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/CardEndpoints.scala @@ -1,9 +1,10 @@ package app.softnetwork.payment.service import app.softnetwork.payment.config.PaymentSettings -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages._ import app.softnetwork.payment.model.{CardPreRegistration, CardView} +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} import sttp.capabilities import sttp.capabilities.akka.AkkaStreams import sttp.model.StatusCode @@ -12,21 +13,22 @@ import sttp.tapir.server.ServerEndpoint import scala.concurrent.Future -trait CardEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler => +trait CardEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { + _: RootPaymentEndpoints[SD] with PaymentHandler => import app.softnetwork.serialization._ val loadCards: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.get + requiredSessionEndpoint.get .in(PaymentSettings.CardRoute) .out( statusCode(StatusCode.Ok).and( jsonBody[Seq[CardView]].description("Authenticated user cards") ) ) - .serverLogic(session => + .serverLogic(principal => _ => { - run(LoadCards(externalUuidWithProfile(session))).map { + run(LoadCards(externalUuidWithProfile(principal._2))).map { case r: CardsLoaded => Right(r.cards.map(_.view)) case other => Left(error(other)) } @@ -35,7 +37,7 @@ trait CardEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler => .description("Load authenticated user cards") val preRegisterCard: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.post + requiredSessionEndpoint.post .in(PaymentSettings.CardRoute) .in(jsonBody[PreRegisterCard]) .out( @@ -44,20 +46,25 @@ trait CardEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler => .description("Card pre registration data") ) ) - .serverLogic(session => + .serverLogic(principal => cmd => { var updatedUser = if (cmd.user.externalUuid.trim.isEmpty) { - cmd.user.withExternalUuid(session.id) + cmd.user.withExternalUuid(principal._2.id) } else { cmd.user } - session.profile match { + principal._2.profile match { case Some(profile) if updatedUser.profile.isEmpty => updatedUser = updatedUser.withProfile(profile) case _ => } - run(cmd.copy(user = updatedUser)).map { + run( + cmd.copy( + user = updatedUser, + clientId = principal._1.map(_.clientId).orElse(principal._2.clientId) + ) + ).map { case r: CardPreRegistered => Right(r.cardPreRegistration) case other => Left(error(other)) } @@ -66,15 +73,15 @@ trait CardEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler => .description("Pre register card") val disableCard: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.delete + requiredSessionEndpoint.delete .in(PaymentSettings.CardRoute) .in(query[String]("cardId").description("Card id to disable")) .out( statusCode(StatusCode.Ok).and(jsonBody[CardDisabled.type]) ) - .serverLogic(session => + .serverLogic(principal => cardId => { - run(DisableCard(externalUuidWithProfile(session), cardId)).map { + run(DisableCard(externalUuidWithProfile(principal._2), cardId)).map { case CardDisabled => Right(CardDisabled) case other => Left(error(other)) } diff --git a/core/src/main/scala/app/softnetwork/payment/service/CardPaymentEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/CardPaymentEndpoints.scala index c6d0f2d..8e7746d 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/CardPaymentEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/CardPaymentEndpoints.scala @@ -1,9 +1,10 @@ package app.softnetwork.payment.service import app.softnetwork.payment.config.PaymentSettings -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages._ -import org.softnetwork.session.model.Session +import app.softnetwork.payment.model.SoftPayAccount +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} import sttp.capabilities import sttp.capabilities.akka.AkkaStreams import sttp.model.{HeaderNames, Method, StatusCode} @@ -13,13 +14,14 @@ import sttp.tapir.server.{PartialServerEndpointWithSecurityOutput, ServerEndpoin import scala.concurrent.Future -trait CardPaymentEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler => +trait CardPaymentEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { + _: RootPaymentEndpoints[SD] with PaymentHandler => import app.softnetwork.serialization._ def payment(payment: Payment): PartialServerEndpointWithSecurityOutput[ (Seq[Option[String]], Option[String], Method, Option[String]), - Session, + (Option[SoftPayAccount.Client], SD), (Option[String], Option[String], Option[String], Option[String], Payment), Any, (Seq[Option[String]], Option[CookieValueWithMeta]), @@ -27,7 +29,7 @@ trait CardPaymentEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler Any, Future ] = - secureEndpoint + requiredSessionEndpoint .in(header[Option[String]](HeaderNames.AcceptLanguage)) .in(header[Option[String]](HeaderNames.Accept)) .in(header[Option[String]](HeaderNames.UserAgent)) @@ -67,13 +69,13 @@ trait CardPaymentEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler ) ) ) - .serverLogic(session => { case (language, accept, userAgent, ipAddress, payment) => + .serverLogic(principal => { case (language, accept, userAgent, ipAddress, payment) => val browserInfo = extractBrowserInfo(language, accept, userAgent, payment) import payment._ run( PreAuthorizeCard( orderUuid, - externalUuidWithProfile(session), + externalUuidWithProfile(principal._2), debitedAmount, currency, registrationId, @@ -155,14 +157,14 @@ trait CardPaymentEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler ) ) ) - .serverLogic(session => { + .serverLogic(principal => { case (language, accept, userAgent, ipAddress, payment, creditedAccount) => val browserInfo = extractBrowserInfo(language, accept, userAgent, payment) import payment._ run( PayIn( orderUuid, - externalUuidWithProfile(session), + externalUuidWithProfile(principal._2), debitedAmount, currency, creditedAccount, @@ -268,14 +270,14 @@ trait CardPaymentEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler ) ) ) - .serverLogic(session => { + .serverLogic(principal => { case (language, accept, userAgent, ipAddress, payment, recurringPaymentRegistrationId) => val browserInfo = extractBrowserInfo(language, accept, userAgent, payment) import payment._ run( PayInFirstRecurring( recurringPaymentRegistrationId, - externalUuidWithProfile(session), + externalUuidWithProfile(principal._2), if (browserInfo.isDefined) Some(ipAddress) else None, browserInfo, statementDescriptor diff --git a/core/src/main/scala/app/softnetwork/payment/service/ClientSession.scala b/core/src/main/scala/app/softnetwork/payment/service/ClientSession.scala new file mode 100644 index 0000000..7c99f04 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/service/ClientSession.scala @@ -0,0 +1,92 @@ +package app.softnetwork.payment.service + +import app.softnetwork.concurrent.Completion +import app.softnetwork.payment.handlers.SoftPayAccountDao +import app.softnetwork.payment.model.{computeExternalUuidWithProfile, SoftPayAccount} +import app.softnetwork.session.model.{SessionData, SessionDataCompanion, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials +import org.softnetwork.session.model.ApiKey +import com.softwaremill.session._ + +import scala.concurrent.Future +import scala.language.implicitConversions +import scala.util.{Failure, Success} + +trait ClientSession[SD <: SessionData with SessionDataDecorator[SD]] extends Completion { + self: SessionMaterials[SD] => + + def softPayAccountDao: SoftPayAccountDao = SoftPayAccountDao + + implicit def sessionConfig: SessionConfig + + implicit def companion: SessionDataCompanion[SD] + + implicit def toSession(client: SoftPayAccount.Client): SD = { + var session = companion.newSession + .withAdmin(false) + .withAnonymous(false) + .withClientId(client.clientId) + session += ("scope", client.accessToken.flatMap(_.scope).getOrElse("")) + session + } + + def encodeClient( + client: SoftPayAccount.Client + ): String = { + clientSessionManager(client).clientSessionManager.encode(client) + } + + def decodeClient(data: String): Option[SD] = manager.clientSessionManager.decode(data).toOption + + def clientSessionManager(client: SoftPayAccount.Client): SessionManager[SD] = { + implicit val innerSessionConfig: SessionConfig = + sessionConfig.copy( + jwt = sessionConfig.jwt.copy( + subject = Some(client.clientId) + ), + serverSecret = client.getClientApiKey + ) + manager(innerSessionConfig, companion) + } + + def clientCookieName: String = sessionConfig.sessionCookieConfig.name + + def sendToClientHeaderName: String = + sessionConfig.sessionHeaderConfig.sendToClientHeaderName + + def getFromClientHeaderName: String = + sessionConfig.sessionHeaderConfig.getFromClientHeaderName + + def sessionManager(clientId: Option[String]): SessionManager[SD] = { + clientId match { + case Some(id) => + softPayAccountDao.loadClient(id) complete () match { + case Success(s) => clientSessionManager(s) + case Failure(_) => manager + } + case _ => manager + } + } + + def clientSessionManager(client: Option[SoftPayAccount.Client]): SessionManager[SD] = { + client match { + case Some(c) => + implicit val innerSessionConfig: SessionConfig = + sessionConfig.copy( + jwt = sessionConfig.jwt.copy( + subject = Some(c.clientId) + ), + serverSecret = c.getClientApiKey + ) + manager(innerSessionConfig, companion) + case _ => manager + } + } + + def loadApiKey(clientId: String): Future[Option[ApiKey]] = + softPayAccountDao.loadApiKey(clientId) + + protected[payment] def externalUuidWithProfile(session: SD): String = + computeExternalUuidWithProfile(session.id, session.profile) + +} diff --git a/core/src/main/scala/app/softnetwork/payment/service/ClientSessionDirectives.scala b/core/src/main/scala/app/softnetwork/payment/service/ClientSessionDirectives.scala new file mode 100644 index 0000000..6a20065 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/service/ClientSessionDirectives.scala @@ -0,0 +1,49 @@ +package app.softnetwork.payment.service + +import akka.http.scaladsl.server.Directives.authenticateOAuth2Async +import akka.http.scaladsl.server.{Directive1, Route} +import akka.http.scaladsl.server.directives.Credentials +import app.softnetwork.account.config.AccountSettings +import app.softnetwork.payment.annotation.InternalApi +import app.softnetwork.payment.model.SoftPayAccount +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.{SessionMaterials, SessionService} + +import scala.concurrent.Future + +trait ClientSessionDirectives[SD <: SessionData with SessionDataDecorator[SD]] + extends SessionService[SD] + with ClientSession[SD] { + _: SessionMaterials[SD] => + + @InternalApi + private[payment] def clientDirective: Directive1[Option[SoftPayAccount.Client]] = + authenticateOAuth2Async(AccountSettings.Realm, oauthClient).optional + + @InternalApi + private[payment] def requiredClientSession( + body: (Option[SoftPayAccount.Client], SD) => Route + ): Route = + clientDirective { client => + requiredSession(sc(clientSessionManager(client)), gt) { session => + body(client, session) + } + } + + @InternalApi + private[payment] def optionalClientSession( + body: (Option[SoftPayAccount.Client], Option[SD]) => Route + ): Route = + clientDirective { client => + optionalSession(sc(clientSessionManager(client)), gt) { session => + body(client, session) + } + } + + @InternalApi + private[payment] def oauthClient: Credentials => Future[Option[SoftPayAccount.Client]] = { + case _ @Credentials.Provided(token) => softPayAccountDao.authenticateClient(Some(token)) + case _ => Future.successful(None) + } + +} diff --git a/core/src/main/scala/app/softnetwork/payment/service/ClientSessionEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/ClientSessionEndpoints.scala new file mode 100644 index 0000000..9ebc5d5 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/service/ClientSessionEndpoints.scala @@ -0,0 +1,150 @@ +package app.softnetwork.payment.service + +import app.softnetwork.account.config.AccountSettings +import app.softnetwork.payment.annotation.InternalApi +import app.softnetwork.payment.model.SoftPayAccount +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.{SessionEndpoints, SessionMaterials} +import com.softwaremill.session.{ + CookieOrHeaderST, + CookieST, + HeaderST, + SessionManager, + SessionResult +} +import org.json4s.Formats +import org.softnetwork.session.model.Session +import sttp.model.headers.WWWAuthenticateChallenge +import sttp.monad.FutureMonad +import sttp.tapir._ +import sttp.tapir.server.PartialServerEndpointWithSecurityOutput + +import scala.concurrent.Future + +trait ClientSessionEndpoints[SD <: SessionData with SessionDataDecorator[SD]] + extends SessionEndpoints[SD] + with ClientSession[SD] { + _: SessionMaterials[SD] => + + implicit def formats: Formats + + import app.softnetwork.session.TapirSessionOptions._ + + protected def clientInput: EndpointInput[Option[String]] = + auth + .bearer[Option[String]](WWWAuthenticateChallenge.bearer(AccountSettings.Realm)) + .description("OAuth2 bearer token") + + @InternalApi + private[payment] def requiredClientSession: PartialServerEndpointWithSecurityOutput[Seq[ + Option[String] + ], (Option[SoftPayAccount.Client], SD), Unit, Unit, Seq[ + Option[String] + ], Unit, Any, Future] = { + val partial = clientSession(Some(true)) + partial.endpoint + .out(partial.securityOutput) + .serverSecurityLogicWithOutput { inputs => + partial.securityLogic(new FutureMonad())(inputs).map { + case Left(l) => Left(l) + case Right(r) => + r._2._2.toOption match { + case Some(session) => Right((r._1, (r._2._1, session))) + case _ => Left(()) + } + } + } + } + + @InternalApi + private[payment] def optionalClientSession: PartialServerEndpointWithSecurityOutput[Seq[ + Option[String] + ], (Option[SoftPayAccount.Client], Option[SD]), Unit, Unit, Seq[ + Option[String] + ], Unit, Any, Future] = { + val partial = clientSession(Some(false)) + partial.endpoint + .out(partial.securityOutput) + .serverSecurityLogicWithOutput { inputs => + partial.securityLogic(new FutureMonad())(inputs).map { + case Left(l) => Left(l) + case Right(r) => Right((r._1, (r._2._1, r._2._2.toOption))) + } + } + } + + @InternalApi + private[payment] def clientSession( + required: Option[Boolean] + ): PartialServerEndpointWithSecurityOutput[Seq[ + Option[String] + ], (Option[SoftPayAccount.Client], SessionResult[SD]), Unit, Unit, Seq[ + Option[String] + ], Unit, Any, Future] = { + val partial = sc.session(gt, required) + partial.endpoint + .prependSecurityIn(clientInput) + .mapSecurityIn(inputs => Seq(inputs._1) ++ inputs._2)(seq => + (seq.head, seq.slice(1, seq.size)) + ) + .out(partial.securityOutput) + .serverSecurityLogicWithOutput { inputs => + softPayAccountDao.authenticateClient(inputs.head) flatMap { client => + implicit val manager: SessionManager[SD] = clientSessionManager(client) + sessionType match { + case Session.SessionType.OneOffCookie | Session.SessionType.OneOffHeader => // oneOff + (gt match { + case CookieST => + sc.sessionLogic(None, inputs.tail.head, None, gt, required) + case HeaderST => + sc.sessionLogic(None, None, inputs.tail.head, gt, required) + case CookieOrHeaderST => + sc.sessionLogic(None, inputs.tail.head, inputs.last, gt, required) + }) match { + case Left(l) => Future.successful(Left(l)) + case Right(r) => Future.successful(Right((r._1, (client, r._2)))) + } + case _ => // refreshable + (gt match { + case CookieST => + oneOff.sessionLogic(None, inputs.tail.head, None, gt, required) match { + case Left(l) => Left(l) + case Right(r) => + refreshable.sessionLogic(Some(r._2), inputs.last, None, gt, required) + } + case HeaderST => + oneOff.sessionLogic(None, None, inputs.tail.head, gt, required) match { + case Left(l) => Left(l) + case Right(r) => + refreshable.sessionLogic(Some(r._2), None, inputs.last, gt, required) + } + case CookieOrHeaderST => + val oneOffInputs = inputs.tail.take(2) + val refreshableInputs = inputs.takeRight(2) + oneOff.sessionLogic( + None, + oneOffInputs.head, + oneOffInputs.last, + gt, + required + ) match { + case Left(l) => Left(l) + case Right(r) => + refreshable.sessionLogic( + Some(r._2), + refreshableInputs.head, + refreshableInputs.last, + gt, + required + ) + } + }) match { + case Left(l) => Future.successful(Left(l)) + case Right(r) => Future.successful(Right((r._1, (client, r._2)))) + } + } + } + } + } + +} diff --git a/core/src/main/scala/app/softnetwork/payment/service/KycDocumentEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/KycDocumentEndpoints.scala index e55b605..e8c6c26 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/KycDocumentEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/KycDocumentEndpoints.scala @@ -2,9 +2,10 @@ package app.softnetwork.payment.service import app.softnetwork.api.server.ApiErrors import app.softnetwork.payment.config.PaymentSettings -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages._ import app.softnetwork.payment.model.{KycDocument, KycDocumentValidationReport} +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} import sttp.capabilities import sttp.capabilities.akka.AkkaStreams import sttp.model.StatusCode @@ -13,12 +14,13 @@ import sttp.tapir.server.ServerEndpoint import scala.concurrent.Future -trait KycDocumentEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler => +trait KycDocumentEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { + _: RootPaymentEndpoints[SD] with PaymentHandler => import app.softnetwork.serialization._ val loadKycDocument: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.get + requiredSessionEndpoint.get .in(PaymentSettings.KycRoute) .in( query[String]("documentType") @@ -34,14 +36,14 @@ trait KycDocumentEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler .description("Kyc document validation report") ) ) - .serverLogic(session => { documentType => + .serverLogic(principal => { documentType => val maybeKycDocumentType: Option[KycDocument.KycDocumentType] = KycDocument.KycDocumentType.enumCompanion.fromName(documentType) maybeKycDocumentType match { case None => Future.successful(Left(ApiErrors.BadRequest("wrong kyc document type"))) case Some(kycDocumentType) => - run(LoadKycDocumentStatus(externalUuidWithProfile(session), kycDocumentType)).map { + run(LoadKycDocumentStatus(externalUuidWithProfile(principal._2), kycDocumentType)).map { case r: KycDocumentStatusLoaded => Right(r.report) case other => Left(error(other)) } @@ -50,7 +52,7 @@ trait KycDocumentEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler .description("Load Kyc document validation report") val addKycDocument: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.post + requiredSessionEndpoint.post .in(PaymentSettings.KycRoute) .in( query[String]("documentType") @@ -67,14 +69,14 @@ trait KycDocumentEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler ) ) ) - .serverLogic(session => { case (documentType, pages) => + .serverLogic(principal => { case (documentType, pages) => val maybeKycDocumentType: Option[KycDocument.KycDocumentType] = KycDocument.KycDocumentType.enumCompanion.fromName(documentType) maybeKycDocumentType match { case None => Future.successful(Left(ApiErrors.BadRequest("wrong kyc document type"))) case Some(kycDocumentType) => - run(AddKycDocument(externalUuidWithProfile(session), pages.bytes, kycDocumentType)) + run(AddKycDocument(externalUuidWithProfile(principal._2), pages.bytes, kycDocumentType)) .map { case r: KycDocumentAdded => Right(r) case other => Left(error(other)) diff --git a/core/src/main/scala/app/softnetwork/payment/service/MandateEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/MandateEndpoints.scala index 665468e..7d70681 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/MandateEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/MandateEndpoints.scala @@ -1,9 +1,10 @@ package app.softnetwork.payment.service import app.softnetwork.payment.config.PaymentSettings -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages._ import app.softnetwork.payment.model.MandateResult +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} import sttp.capabilities import sttp.capabilities.akka.AkkaStreams import sttp.model.StatusCode @@ -12,12 +13,13 @@ import sttp.tapir.server.ServerEndpoint import scala.concurrent.Future -trait MandateEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler => +trait MandateEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { + _: RootPaymentEndpoints[SD] with PaymentHandler => import app.softnetwork.serialization._ val createMandate: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.post + requiredSessionEndpoint.post .in(PaymentSettings.MandateRoute) .out( oneOf[PaymentResult]( @@ -32,9 +34,14 @@ trait MandateEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler => ) ) ) - .serverLogic(session => + .serverLogic(principal => _ => - run(CreateMandate(externalUuidWithProfile(session))).map { + run( + CreateMandate( + externalUuidWithProfile(principal._2), + clientId = principal._1.map(_.clientId).orElse(principal._2.clientId) + ) + ).map { case MandateCreated => Right(MandateCreated) case r: MandateConfirmationRequired => Right(r) case other => Left(error(other)) @@ -43,19 +50,24 @@ trait MandateEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler => .description("Create a mandate for the authenticated payment account") val cancelMandate: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.delete + requiredSessionEndpoint.delete .in(PaymentSettings.MandateRoute) .out( statusCode(StatusCode.Ok) .and(jsonBody[MandateCanceled.type].description("Mandate canceled")) ) - .serverLogic(session => + .serverLogic { case (client, session) => _ => - run(CancelMandate(externalUuidWithProfile(session))).map { + run( + CancelMandate( + externalUuidWithProfile(session), + clientId = client.map(_.clientId).orElse(session.clientId) + ) + ).map { case MandateCanceled => Right(MandateCanceled) case other => Left(error(other)) } - ) + } .description("Create Mandate for the authenticated payment account") val updateMandateStatus: ServerEndpoint[Any with AkkaStreams, Future] = diff --git a/core/src/main/scala/app/softnetwork/payment/service/GenericPaymentService.scala b/core/src/main/scala/app/softnetwork/payment/service/PaymentService.scala similarity index 90% rename from core/src/main/scala/app/softnetwork/payment/service/GenericPaymentService.scala rename to core/src/main/scala/app/softnetwork/payment/service/PaymentService.scala index 1d40880..72fd6fd 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/GenericPaymentService.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/PaymentService.scala @@ -1,9 +1,10 @@ package app.softnetwork.payment.service +import akka.actor.typed.ActorSystem import akka.http.scaladsl.model.{HttpResponse, StatusCodes} import akka.http.scaladsl.server.{Directives, Route} import app.softnetwork.api.server.DefaultComplete -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages._ import app.softnetwork.payment.serialization._ import app.softnetwork.payment.config.PaymentSettings @@ -14,38 +15,40 @@ import akka.http.scaladsl.server.directives.FileInfo import akka.stream.Materializer import akka.stream.scaladsl.{Sink, Source} import akka.util.ByteString -import app.softnetwork.session.service.SessionService +import app.softnetwork.session.service.{ServiceWithSessionDirectives, SessionMaterials} import com.softwaremill.session.CsrfDirectives.hmacTokenCsrfProtection import com.softwaremill.session.CsrfOptions.checkHeader import com.typesafe.scalalogging.StrictLogging import de.heikoseeberger.akkahttpjson4s.Json4sSupport import org.json4s.{jackson, Formats} import org.json4s.jackson.Serialization -import org.softnetwork.session.model.Session import app.softnetwork.api.server._ import app.softnetwork.payment.config.PaymentSettings._ import app.softnetwork.payment.model._ +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import com.softwaremill.session.SessionConfig import java.io.ByteArrayOutputStream import scala.concurrent.Await - import scala.language.implicitConversions -trait GenericPaymentService +trait PaymentService[SD <: SessionData with SessionDataDecorator[SD]] extends Directives with DefaultComplete with Json4sSupport with StrictLogging with BasicPaymentService - with ApiRoute { _: GenericPaymentHandler => + with ServiceWithSessionDirectives[PaymentCommand, PaymentResult, SD] + with ClientSessionDirectives[SD] + with ApiRoute { _: PaymentHandler with SessionMaterials[SD] => implicit def serialization: Serialization.type = jackson.Serialization implicit def formats: Formats = paymentFormats - import Session._ + implicit def sessionConfig: SessionConfig - def sessionService: SessionService + override implicit def ts: ActorSystem[_] = system val route: Route = { pathPrefix(PaymentSettings.PaymentPath) { @@ -68,7 +71,7 @@ trait GenericPaymentService // check anti CSRF token hmacTokenCsrfProtection(checkHeader) { // check if a session exists - sessionService.requiredSession { session => + requiredClientSession { (client, session) => pathEnd { get { run(LoadCards(externalUuidWithProfile(session))) completeWith { @@ -95,7 +98,12 @@ trait GenericPaymentService updatedUser = updatedUser.withProfile(profile) case _ => } - run(cmd.copy(user = updatedUser)) completeWith { + run( + cmd.copy( + user = updatedUser, + clientId = client.map(_.clientId).orElse(session.clientId) + ) + ) completeWith { case r: CardPreRegistered => complete( HttpResponse( @@ -124,10 +132,15 @@ trait GenericPaymentService // check anti CSRF token hmacTokenCsrfProtection(checkHeader) { // check if a session exists - sessionService.requiredSession { session => + requiredClientSession { (client, session) => get { pathEnd { - run(LoadPaymentAccount(externalUuidWithProfile(session))) completeWith { + run( + LoadPaymentAccount( + externalUuidWithProfile(session), + clientId = client.map(_.clientId).orElse(session.clientId) + ) + ) completeWith { case r: PaymentAccountLoaded => complete(HttpResponse(StatusCodes.OK, entity = r.paymentAccount.view)) case other => error(other) @@ -363,10 +376,15 @@ trait GenericPaymentService // check anti CSRF token hmacTokenCsrfProtection(checkHeader) { // check if a session exists - sessionService.requiredSession { session => + requiredClientSession { (client, session) => pathEnd { get { - run(LoadBankAccount(externalUuidWithProfile(session))) completeWith { + run( + LoadBankAccount( + externalUuidWithProfile(session), + clientId = client.map(_.clientId).orElse(session.clientId) + ) + ) completeWith { case r: BankAccountLoaded => complete( HttpResponse( @@ -421,7 +439,8 @@ trait GenericPaymentService externalUuidWithProfile(session), bankAccount.withExternalUuid(externalUuid), updatedUser, - acceptedTermsOfPSP + acceptedTermsOfPSP, + client.map(_.clientId).orElse(session.clientId) ) ) completeWith { case r: BankAccountCreatedOrUpdated => @@ -445,7 +464,7 @@ trait GenericPaymentService // check anti CSRF token hmacTokenCsrfProtection(checkHeader) { // check if a session exists - sessionService.requiredSession { session => + requiredClientSession { (_, session) => pathEnd { get { run(GetUboDeclaration(externalUuidWithProfile(session))) completeWith { @@ -485,7 +504,7 @@ trait GenericPaymentService // check anti CSRF token hmacTokenCsrfProtection(checkHeader) { // check if a session exists - sessionService.requiredSession { session => + requiredClientSession { (_, session) => pathEnd { get { run( @@ -543,9 +562,14 @@ trait GenericPaymentService // check anti CSRF token hmacTokenCsrfProtection(checkHeader) { // check if a session exists - sessionService.requiredSession { session => + requiredClientSession { (client, session) => post { - run(CreateMandate(externalUuidWithProfile(session))) completeWith { + run( + CreateMandate( + externalUuidWithProfile(session), + clientId = client.map(_.clientId).orElse(session.clientId) + ) + ) completeWith { case r: MandateConfirmationRequired => complete(HttpResponse(StatusCodes.OK, entity = r)) case MandateCreated => complete(HttpResponse(StatusCodes.OK)) @@ -553,7 +577,12 @@ trait GenericPaymentService } } ~ delete { - run(CancelMandate(externalUuidWithProfile(session))) completeWith { + run( + CancelMandate( + externalUuidWithProfile(session), + clientId = client.map(_.clientId).orElse(session.clientId) + ) + ) completeWith { case MandateCanceled => complete(HttpResponse(StatusCodes.OK)) case other => error(other) } @@ -566,7 +595,7 @@ trait GenericPaymentService // check anti CSRF token hmacTokenCsrfProtection(checkHeader) { // check if a session exists - sessionService.requiredSession { session => + requiredClientSession { (client, session) => get { pathPrefix(Segment) { recurringPaymentRegistrationId => run( @@ -579,7 +608,12 @@ trait GenericPaymentService } } ~ post { entity(as[RegisterRecurringPayment]) { cmd => - run(cmd.copy(debitedAccount = externalUuidWithProfile(session))) completeWith { + run( + cmd.copy( + debitedAccount = externalUuidWithProfile(session), + clientId = client.map(_.clientId).orElse(session.clientId) + ) + ) completeWith { case r: RecurringPaymentRegistered => complete(HttpResponse(StatusCodes.OK, entity = r)) case r: MandateConfirmationRequired => diff --git a/core/src/main/scala/app/softnetwork/payment/service/GenericPaymentEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/PaymentServiceEndpoints.scala similarity index 63% rename from core/src/main/scala/app/softnetwork/payment/service/GenericPaymentEndpoints.scala rename to core/src/main/scala/app/softnetwork/payment/service/PaymentServiceEndpoints.scala index feead6d..7815d51 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/GenericPaymentEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/PaymentServiceEndpoints.scala @@ -1,8 +1,10 @@ package app.softnetwork.payment.service -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages._ import app.softnetwork.payment.model._ +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials import sttp.capabilities import sttp.capabilities.akka.AkkaStreams import sttp.model.Part @@ -13,16 +15,16 @@ import sttp.tapir.server.ServerEndpoint.Full import scala.concurrent.Future import scala.language.{implicitConversions, postfixOps} -trait GenericPaymentEndpoints - extends RootPaymentEndpoints - with CardEndpoints - with CardPaymentEndpoints - with BankAccountEndpoints - with KycDocumentEndpoints - with UboDeclarationEndpoints - with RecurringPaymentEndpoints - with MandateEndpoints { - _: GenericPaymentHandler => +trait PaymentServiceEndpoints[SD <: SessionData with SessionDataDecorator[SD]] + extends RootPaymentEndpoints[SD] + with CardEndpoints[SD] + with CardPaymentEndpoints[SD] + with BankAccountEndpoints[SD] + with KycDocumentEndpoints[SD] + with UboDeclarationEndpoints[SD] + with RecurringPaymentEndpoints[SD] + with MandateEndpoints[SD] { + _: PaymentHandler with SessionMaterials[SD] => import app.softnetwork.serialization._ @@ -31,16 +33,21 @@ trait GenericPaymentEndpoints def hooks: Full[Unit, Unit, (String, String), Unit, Unit, Any, Future] val loadPaymentAccount: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.get + requiredSessionEndpoint.get .out(jsonBody[PaymentAccountView].description("Authenticated user payment account")) - .serverLogic(session => + .serverLogic { case (client, session) => _ => { - run(LoadPaymentAccount(externalUuidWithProfile(session))).map { + run( + LoadPaymentAccount( + externalUuidWithProfile(session), + clientId = client.map(_.clientId).orElse(session.clientId) + ) + ).map { case r: PaymentAccountLoaded => Right(r.paymentAccount.view) case other => Left(error(other)) } } - ) + } .description("Load authenticated user payment account") override val endpoints: List[ServerEndpoint[AkkaStreams with capabilities.WebSockets, Future]] = diff --git a/core/src/main/scala/app/softnetwork/payment/service/RecurringPaymentEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/RecurringPaymentEndpoints.scala index 4a1e53f..0a4e8a4 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/RecurringPaymentEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/RecurringPaymentEndpoints.scala @@ -1,9 +1,10 @@ package app.softnetwork.payment.service import app.softnetwork.payment.config.PaymentSettings -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages._ import app.softnetwork.payment.model.{RecurringPayment, RecurringPaymentView} +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} import sttp.capabilities import sttp.capabilities.akka.AkkaStreams import sttp.model.StatusCode @@ -12,12 +13,13 @@ import sttp.tapir.server.ServerEndpoint import scala.concurrent.Future -trait RecurringPaymentEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler => +trait RecurringPaymentEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { + _: RootPaymentEndpoints[SD] with PaymentHandler => import app.softnetwork.serialization._ val registerRecurringPayment: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.post + requiredSessionEndpoint.post .in(PaymentSettings.RecurringPaymentRoute) .in(jsonBody[RegisterRecurringPayment].description("Recurring payment to register")) .out( @@ -38,18 +40,23 @@ trait RecurringPaymentEndpoints { _: RootPaymentEndpoints with GenericPaymentHan ) ) ) - .serverLogic(session => + .serverLogic { case (client, session) => cmd => - run(cmd.copy(debitedAccount = externalUuidWithProfile(session))).map { + run( + cmd.copy( + debitedAccount = externalUuidWithProfile(session), + clientId = client.map(_.clientId).orElse(session.clientId) + ) + ).map { case r: RecurringPaymentRegistered => Right(r) case r: MandateConfirmationRequired => Right(r) case other => Left(error(other)) } - ) + } .description("Register a recurring payment for the authenticated payment account") val loadRecurringPayment: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.get + requiredSessionEndpoint.get .in(PaymentSettings.RecurringPaymentRoute) .in(path[String]) .out( @@ -58,11 +65,11 @@ trait RecurringPaymentEndpoints { _: RootPaymentEndpoints with GenericPaymentHan .description("Recurring payment successfully loaded") ) ) - .serverLogic(session => + .serverLogic(principal => recurringPaymentRegistrationId => run( LoadRecurringPayment( - externalUuidWithProfile(session), + externalUuidWithProfile(principal._2), recurringPaymentRegistrationId ) ).map { @@ -73,7 +80,7 @@ trait RecurringPaymentEndpoints { _: RootPaymentEndpoints with GenericPaymentHan .description("Load the recurring payment of the authenticated payment account") val updateRecurringCardPaymentRegistration: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.put + requiredSessionEndpoint.put .in(PaymentSettings.RecurringPaymentRoute) .in( jsonBody[UpdateRecurringCardPaymentRegistration].description( @@ -87,9 +94,9 @@ trait RecurringPaymentEndpoints { _: RootPaymentEndpoints with GenericPaymentHan .description("Recurring card payment successfully updated") ) ) - .serverLogic(session => + .serverLogic(principal => cmd => - run(cmd.copy(debitedAccount = externalUuidWithProfile(session))).map { + run(cmd.copy(debitedAccount = externalUuidWithProfile(principal._2))).map { case r: RecurringCardPaymentRegistrationUpdated => Right(r.result) case other => Left(error(other)) } @@ -99,18 +106,18 @@ trait RecurringPaymentEndpoints { _: RootPaymentEndpoints with GenericPaymentHan ) val deleteRecurringPayment: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.delete + requiredSessionEndpoint.delete .in(PaymentSettings.RecurringPaymentRoute) .in(path[String]) .out( statusCode(StatusCode.Ok) .and(jsonBody[RecurringPayment.RecurringCardPaymentResult]) ) - .serverLogic(session => + .serverLogic(principal => recurringPaymentRegistrationId => run( UpdateRecurringCardPaymentRegistration( - externalUuidWithProfile(session), + externalUuidWithProfile(principal._2), recurringPaymentRegistrationId, None, Some(RecurringPayment.RecurringCardPaymentStatus.ENDED) diff --git a/core/src/main/scala/app/softnetwork/payment/service/RootPaymentEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/RootPaymentEndpoints.scala index 19140bc..cb0be8c 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/RootPaymentEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/RootPaymentEndpoints.scala @@ -1,13 +1,15 @@ package app.softnetwork.payment.service +import akka.actor.typed.ActorSystem import app.softnetwork.api.server.ApiErrors import app.softnetwork.payment.config.PaymentSettings -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages._ +import app.softnetwork.payment.model.SoftPayAccount import app.softnetwork.payment.serialization.paymentFormats -import app.softnetwork.session.service.ServiceWithSessionEndpoints +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.{ServiceWithSessionEndpoints, SessionMaterials} import org.json4s.Formats -import org.softnetwork.session.model.Session import sttp.model.headers.CookieValueWithMeta import sttp.model.Method import sttp.tapir.server.PartialServerEndpointWithSecurityOutput @@ -16,22 +18,25 @@ import sttp.tapir.Endpoint import scala.concurrent.Future import scala.language.implicitConversions -trait RootPaymentEndpoints +trait RootPaymentEndpoints[SD <: SessionData with SessionDataDecorator[SD]] extends BasicPaymentService - with ServiceWithSessionEndpoints[PaymentCommand, PaymentResult] { - _: GenericPaymentHandler => + with ServiceWithSessionEndpoints[PaymentCommand, PaymentResult, SD] + with ClientSessionEndpoints[SD] { + _: PaymentHandler with SessionMaterials[SD] => override implicit def formats: Formats = paymentFormats + override implicit def ts: ActorSystem[_] = system + override implicit def resultToApiError(result: PaymentResult): ApiErrors.ErrorInfo = error(result) lazy val rootEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = endpoint .in(PaymentSettings.PaymentPath) - lazy val secureEndpoint: PartialServerEndpointWithSecurityOutput[ + lazy val requiredSessionEndpoint: PartialServerEndpointWithSecurityOutput[ (Seq[Option[String]], Option[String], Method, Option[String]), - Session, + (Option[SoftPayAccount.Client], SD), Unit, Any, (Seq[Option[String]], Option[CookieValueWithMeta]), @@ -41,7 +46,28 @@ trait RootPaymentEndpoints ] = ApiErrors .withApiErrorVariants( - antiCsrfWithRequiredSession(sc, gt, checkMode) + hmacTokenCsrfProtection(checkMode) { + requiredClientSession + } ) .in(PaymentSettings.PaymentPath) + + lazy val optionalSessionEndpoint: PartialServerEndpointWithSecurityOutput[ + (Seq[Option[String]], Option[String], Method, Option[String]), + (Option[SoftPayAccount.Client], Option[SD]), + Unit, + Any, + (Seq[Option[String]], Option[CookieValueWithMeta]), + Unit, + Any, + Future + ] = + ApiErrors + .withApiErrorVariants( + hmacTokenCsrfProtection(checkMode) { + optionalClientSession + } + ) + .in(PaymentSettings.PaymentPath) + } diff --git a/core/src/main/scala/app/softnetwork/payment/service/SoftPayAccountService.scala b/core/src/main/scala/app/softnetwork/payment/service/SoftPayAccountService.scala new file mode 100644 index 0000000..245d9a8 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/service/SoftPayAccountService.scala @@ -0,0 +1,38 @@ +package app.softnetwork.payment.service + +import akka.http.scaladsl.server.Route +import app.softnetwork.account.config.AccountSettings +import app.softnetwork.account.service.BasicAccountService +import app.softnetwork.payment.handlers.SoftPayAccountTypeKey +import app.softnetwork.payment.serialization.paymentFormats +import app.softnetwork.session.config.Settings +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials +import com.softwaremill.session.SessionConfig +import org.json4s.Formats + +trait SoftPayAccountService[SD <: SessionData with SessionDataDecorator[SD]] + extends BasicAccountService[SD] + with SoftPayAccountTypeKey { + _: SessionMaterials[SD] => + + implicit def sessionConfig: SessionConfig = Settings.Session.DefaultSessionConfig + + override implicit lazy val formats: Formats = paymentFormats + + override val route: Route = { + pathPrefix(AccountSettings.Path) { + signUp ~ + login ~ + activate ~ + logout ~ + verificationCode ~ + resetPasswordToken ~ + resetPassword ~ + unsubscribe ~ + device ~ + password + } + } + +} diff --git a/core/src/main/scala/app/softnetwork/payment/service/SoftPayAccountServiceEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/SoftPayAccountServiceEndpoints.scala new file mode 100644 index 0000000..17feef1 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/service/SoftPayAccountServiceEndpoints.scala @@ -0,0 +1,44 @@ +package app.softnetwork.payment.service + +import app.softnetwork.account.service.BasicAccountServiceEndpoints +import app.softnetwork.payment.handlers.SoftPayAccountTypeKey +import app.softnetwork.payment.serialization.paymentFormats +import app.softnetwork.session.config.Settings +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials +import com.softwaremill.session.SessionConfig +import org.json4s.Formats +import sttp.capabilities +import sttp.capabilities.akka.AkkaStreams +import sttp.tapir.server.ServerEndpoint + +import scala.concurrent.Future + +trait SoftPayAccountServiceEndpoints[SD <: SessionData with SessionDataDecorator[SD]] + extends BasicAccountServiceEndpoints[SD] + with SoftPayAccountTypeKey { _: SessionMaterials[SD] => + + implicit def sessionConfig: SessionConfig = Settings.Session.DefaultSessionConfig + + override implicit lazy val formats: Formats = paymentFormats + + override lazy val endpoints + : List[ServerEndpoint[AkkaStreams with capabilities.WebSockets, Future]] = + List( + signUp, + basic, + login, + signIn, + activate, + logout, + signOut, + sendVerificationCode, + sendResetPasswordToken, + checkResetPasswordToken, + resetPassword, + unsubscribe, + registerDevice, + unregisterDevice, + updatePassword + ) +} diff --git a/core/src/main/scala/app/softnetwork/payment/service/SoftPayOAuthService.scala b/core/src/main/scala/app/softnetwork/payment/service/SoftPayOAuthService.scala new file mode 100644 index 0000000..75bbb42 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/service/SoftPayOAuthService.scala @@ -0,0 +1,118 @@ +package app.softnetwork.payment.service + +import akka.http.scaladsl.model.StatusCodes +import akka.http.scaladsl.model.headers.BasicHttpCredentials +import akka.http.scaladsl.server.{AuthenticationFailedRejection, RejectionHandler, Route} +import app.softnetwork.account.config.AccountSettings +import app.softnetwork.account.message.{ + AccessTokenGenerated, + AccessTokenRefreshed, + AccountErrorMessage, + Tokens +} +import app.softnetwork.account.service.OAuthService +import app.softnetwork.payment.handlers.SoftPayAccountTypeKey +import app.softnetwork.payment.message.AccountMessages.{GenerateClientToken, RefreshClientToken} +import app.softnetwork.payment.serialization.paymentFormats +import app.softnetwork.session.config.Settings +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials +import com.softwaremill.session.SessionConfig +import org.json4s.Formats + +trait SoftPayOAuthService[SD <: SessionData with SessionDataDecorator[SD]] + extends OAuthService[SD] + with SoftPayAccountTypeKey + with ClientSessionDirectives[SD] { + _: SessionMaterials[SD] => + + implicit def sessionConfig: SessionConfig = Settings.Session.DefaultSessionConfig + + override implicit lazy val formats: Formats = paymentFormats + + override val route: Route = { + pathPrefix(AccountSettings.OAuthPath) { + concat(token ~ me :: (signin ++ backup).toList: _*) + } + } + + override lazy val token: Route = + path("token") { + post { + formField("grant_type") { + case "client_credentials" => + formField("credentials") { credentials => + val httpCredentials = BasicHttpCredentials(credentials) + val clientId = httpCredentials.username + val clientSecret = httpCredentials.password + run(clientId, GenerateClientToken(clientId, clientSecret)) completeWith { + case r: AccessTokenGenerated => + complete( + StatusCodes.OK, + Tokens( + r.accessToken.token, + r.accessToken.tokenType.toLowerCase(), + r.accessToken.expiresIn, + r.accessToken.refreshToken, + r.accessToken.refreshExpiresIn + ) + ) + case error: AccountErrorMessage => + complete( + StatusCodes.BadRequest, + Map( + "error" -> "access_denied", + "error_description" -> error.message + ) + ) + case _ => complete(StatusCodes.BadRequest) + } + } + case "refresh_token" => + formField("refresh_token") { refreshToken => + run(refreshToken, RefreshClientToken(refreshToken)) completeWith { + case r: AccessTokenRefreshed => + complete( + StatusCodes.OK, + Tokens( + r.accessToken.token, + r.accessToken.tokenType.toLowerCase(), + r.accessToken.expiresIn, + r.accessToken.refreshToken, + r.accessToken.refreshExpiresIn + ) + ) + case error: AccountErrorMessage => + complete( + StatusCodes.BadRequest, + Map( + "error" -> "access_denied", + "error_description" -> error.message + ) + ) + case _ => complete(StatusCodes.BadRequest) + } + } + case _ => complete(StatusCodes.BadRequest) + } + } + } + + override lazy val me: Route = path("me") { + get { + handleRejections( + RejectionHandler + .newBuilder() + .handleAll[AuthenticationFailedRejection](authenticationFailedRejectionHandler) + .result() + ) { + authenticateOAuth2Async(AccountSettings.Realm, oauthClient) { client => + setSession(sc(clientSessionManager(client)), st, client.asInstanceOf[SD]) { + complete(StatusCodes.OK) + } + } + } + } + } + +} diff --git a/core/src/main/scala/app/softnetwork/payment/service/SoftPayOAuthServiceEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/SoftPayOAuthServiceEndpoints.scala new file mode 100644 index 0000000..252613a --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/service/SoftPayOAuthServiceEndpoints.scala @@ -0,0 +1,205 @@ +package app.softnetwork.payment.service + +import akka.http.scaladsl.model.headers.BasicHttpCredentials +import app.softnetwork.account.config.AccountSettings +import app.softnetwork.account.message.{ + AccessTokenGenerated, + AccessTokenRefreshed, + AccountErrorMessage, + BearerAuthenticationFailed, + Tokens +} +import app.softnetwork.account.service.OAuthServiceEndpoints +import app.softnetwork.api.server.ApiErrors +import app.softnetwork.payment.handlers.SoftPayAccountTypeKey +import app.softnetwork.payment.message.AccountMessages.{ + GenerateClientToken, + OAuthClient, + OAuthClientSucceededResult, + RefreshClientToken +} +import app.softnetwork.payment.serialization.paymentFormats +import app.softnetwork.session.config.Settings +import app.softnetwork.session.httpCookieToTapirCookieWithMeta +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials +import com.softwaremill.session.{CookieST, HeaderST, SessionConfig} +import org.json4s.Formats +import sttp.capabilities +import sttp.capabilities.akka.AkkaStreams +import sttp.model.headers.WWWAuthenticateChallenge +import sttp.tapir.EndpointIO.Example +import sttp.tapir.json.json4s.jsonBody +import sttp.tapir.server.ServerEndpoint + +import scala.concurrent.Future + +trait SoftPayOAuthServiceEndpoints[SD <: SessionData with SessionDataDecorator[SD]] + extends OAuthServiceEndpoints[SD] + with SoftPayAccountTypeKey + with ClientSession[SD] { _: SessionMaterials[SD] => + + import app.softnetwork.serialization.serialization + + implicit def sessionConfig: SessionConfig = Settings.Session.DefaultSessionConfig + + override implicit lazy val formats: Formats = paymentFormats + + override lazy val endpoints + : List[ServerEndpoint[AkkaStreams with capabilities.WebSockets, Future]] = + List( + token, + me + ) ++ services.map(signin) ++ services.map(backup) + + override val token: ServerEndpoint[Any with AkkaStreams, Future] = + endpoint.post + .in(AccountSettings.OAuthPath / "token") + .description("OAuth2 token endpoint") + .errorOut(ApiErrors.oneOfApiErrors) + .in( + formBody[Map[String, String]] + .description("form body") + .map[ClientTokenRequest](m => ClientTokenRequest.decode(m))(ClientTokenRequest.encode) + .example( + Example.of( + ClientCredentials( + "SplxlOBeZQQYbYS6WxSbIA", + Some("*") + ), + Some("client credentials request") + ) + ) + .example( + Example.of( + RefreshToken("tGzv3JOkF0XG5Qx2TlKWIA"), + Some("refresh token request") + ) + ) + ) + .out(jsonBody[Tokens]) + .serverLogic { + case tokenRequest: ClientCredentials => + import tokenRequest._ + val httpCredentials = BasicHttpCredentials(credentials) + val clientId = httpCredentials.username + val clientSecret = httpCredentials.password + run(clientId, GenerateClientToken(clientId, clientSecret)) map { + case r: AccessTokenGenerated => + Right( + Tokens( + r.accessToken.token, + r.accessToken.tokenType.toLowerCase(), + r.accessToken.expiresIn, + r.accessToken.refreshToken, + r.accessToken.refreshExpiresIn + ) + ) + case error: AccountErrorMessage => + Left(ApiErrors.BadRequest(error.message)) + case _ => Left(ApiErrors.BadRequest("Unknown")) + } + case tokenRequest: RefreshToken => + import tokenRequest._ + run(refreshToken, RefreshClientToken(refreshToken)) map { + case r: AccessTokenRefreshed => + Right( + Tokens( + r.accessToken.token, + r.accessToken.tokenType.toLowerCase(), + r.accessToken.expiresIn, + r.accessToken.refreshToken, + r.accessToken.refreshExpiresIn + ) + ) + case error: AccountErrorMessage => + Left(ApiErrors.BadRequest(error.message)) + case _ => Left(ApiErrors.BadRequest("Unknown")) + } + case tokenRequest: UnsupportedGrantType => + Future.successful( + Left(ApiErrors.BadRequest(s"Unknown grant_type ${tokenRequest.grantType}")) + ) + } + + override val me: ServerEndpoint[Any with AkkaStreams, Future] = + endpoint.get + .in(AccountSettings.OAuthPath / "me") + .description("OAuth2 me endpoint") + .securityIn(auth.bearer[String](WWWAuthenticateChallenge.bearer(AccountSettings.Realm))) + .errorOut(ApiErrors.oneOfApiErrors) + .out(setCookieOpt(clientCookieName)) + .out(header[Option[String]](sendToClientHeaderName)) + .serverSecurityLogicWithOutput[Unit, Future](token => + run(token, OAuthClient(token)) map { + case r: OAuthClientSucceededResult => + val encoded = encodeClient(r.client) + Right( + st match { + case HeaderST => + (None, Some(encoded)) + case CookieST => + ( + Some(manager.clientSessionManager.createCookieWithValue(encoded).valueWithMeta), + None + ) + }, + () + ) + + case _ => Left(resultToApiError(BearerAuthenticationFailed)) + } + ) + .serverLogic { _ => _ => + Future.successful( + Right(()) + ) + } + +} + +sealed trait ClientTokenRequest { + def asMap(): Map[String, String] +} + +case class ClientCredentials(credentials: String, scope: Option[String] = None) + extends ClientTokenRequest { + override def asMap(): Map[String, String] = + Map( + "grant_type" -> "client_credentials", + "credentials" -> credentials, + "scope" -> scope.getOrElse("") + ) +} + +case class RefreshToken(refreshToken: String) extends ClientTokenRequest { + override def asMap(): Map[String, String] = + Map( + "grant_type" -> "refresh_token", + "refresh_token" -> refreshToken + ) +} + +case class UnsupportedGrantType(grantType: String) extends ClientTokenRequest { + override def asMap(): Map[String, String] = + Map( + "grant_type" -> grantType + ) +} + +object ClientTokenRequest { + def decode(form: Map[String, String]): ClientTokenRequest = { + form.getOrElse("grant_type", "") match { + case "client_credentials" => + ClientCredentials( + form("credentials"), + form.get("scope") + ) + case "refresh_token" => + RefreshToken(form("refresh_token")) + case other => UnsupportedGrantType(other) + } + } + + def encode(tokenRequest: ClientTokenRequest): Map[String, String] = tokenRequest.asMap() +} 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 6f93906..c7878f7 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala @@ -1,9 +1,10 @@ package app.softnetwork.payment.service import app.softnetwork.payment.config.PaymentSettings -import app.softnetwork.payment.handlers.GenericPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages._ import app.softnetwork.payment.model.{UboDeclaration, UboDeclarationView} +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} import sttp.capabilities import sttp.capabilities.akka.AkkaStreams import sttp.model.StatusCode @@ -12,12 +13,13 @@ import sttp.tapir.server.ServerEndpoint import scala.concurrent.Future -trait UboDeclarationEndpoints { _: RootPaymentEndpoints with GenericPaymentHandler => +trait UboDeclarationEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { + _: RootPaymentEndpoints[SD] with PaymentHandler => import app.softnetwork.serialization._ val addUboDeclaration: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.post + requiredSessionEndpoint.post .in(PaymentSettings.DeclarationRoute) .in( jsonBody[UboDeclaration.UltimateBeneficialOwner] @@ -30,8 +32,8 @@ trait UboDeclarationEndpoints { _: RootPaymentEndpoints with GenericPaymentHandl .description("The UBO successfully recorded") ) ) - .serverLogic(session => { ubo => - run(CreateOrUpdateUbo(externalUuidWithProfile(session), ubo)).map { + .serverLogic(principal => { ubo => + run(CreateOrUpdateUbo(externalUuidWithProfile(principal._2), ubo)).map { case r: UboCreatedOrUpdated => Right(r.ubo) case other => Left(error(other)) } @@ -39,7 +41,7 @@ trait UboDeclarationEndpoints { _: RootPaymentEndpoints with GenericPaymentHandl .description("Record an UBO for the authenticated legal payment account") val loadUboDeclaration: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.get + requiredSessionEndpoint.get .in(PaymentSettings.DeclarationRoute) .out( statusCode(StatusCode.Ok) @@ -48,9 +50,9 @@ trait UboDeclarationEndpoints { _: RootPaymentEndpoints with GenericPaymentHandl .description("Ubo declaration of the authenticated legal payment account") ) ) - .serverLogic(session => + .serverLogic(principal => _ => - run(GetUboDeclaration(externalUuidWithProfile(session))).map { + run(GetUboDeclaration(externalUuidWithProfile(principal._2))).map { case r: UboDeclarationLoaded => Right(r.declaration.view) case other => Left(error(other)) } @@ -58,7 +60,7 @@ trait UboDeclarationEndpoints { _: RootPaymentEndpoints with GenericPaymentHandl .description("Load the Ubo declaration of the authenticated legal payment account") val validateUboDeclaration: ServerEndpoint[Any with AkkaStreams, Future] = - secureEndpoint.put + requiredSessionEndpoint.put .in(PaymentSettings.DeclarationRoute) .out( statusCode(StatusCode.Ok).and( @@ -67,9 +69,9 @@ trait UboDeclarationEndpoints { _: RootPaymentEndpoints with GenericPaymentHandl ) ) ) - .serverLogic(session => + .serverLogic(principal => _ => - run(ValidateUboDeclaration(externalUuidWithProfile(session))).map { + run(ValidateUboDeclaration(externalUuidWithProfile(principal._2))).map { case UboDeclarationAskedForValidation => Right(UboDeclarationAskedForValidation) case other => Left(error(other)) } diff --git a/mangopay/api/build.sbt b/mangopay/api/build.sbt index 04ae0cf..986d3dc 100644 --- a/mangopay/api/build.sbt +++ b/mangopay/api/build.sbt @@ -27,5 +27,5 @@ organization := "app.softnetwork.payment" name := "mangopay-api" libraryDependencies ++= Seq( - "app.softnetwork.scheduler" %% "scheduler-api" % Versions.scheduler + "app.softnetwork.notification" %% "notification-api" % Versions.notification ) diff --git a/mangopay/api/src/main/resources/reference.conf b/mangopay/api/src/main/resources/reference.conf index 95e0548..4e7797c 100644 --- a/mangopay/api/src/main/resources/reference.conf +++ b/mangopay/api/src/main/resources/reference.conf @@ -1,10 +1,14 @@ include "softnetwork-jdbc-persistence.conf" include "softnetwork-scheduler.conf" -softnetwork.api.name = "softnetwork-mangopay" +softnetwork.api.name = "softpayment" softnetwork.api.version = "0.5.0" -softnetwork.api.server.port = 9000 softnetwork.api.server.request-timeout = 120 s -softnetwork.api.server.swagger-path-prefix = ["swagger", "payment"] +softnetwork.api.server.swagger-path-prefix = ["swagger", "api"] -akka.cluster.roles = [${payment.akka-node-role}, ${softnetwork.scheduler.akka-node-role}] +akka.cluster.roles = [ + ${payment.akka-node-role}, + ${auth.akka-node-role}, + ${notification.akka-node-role}, + ${softnetwork.scheduler.akka-node-role} +] diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/BasicServiceEndpoint.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/BasicServiceEndpoint.scala deleted file mode 100644 index 1cb686a..0000000 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/BasicServiceEndpoint.scala +++ /dev/null @@ -1,67 +0,0 @@ -package app.softnetwork.payment.api - -import akka.actor.typed.ActorSystem -import app.softnetwork.api.server.ApiEndpoint -import app.softnetwork.session.service.SessionEndpoints -import com.softwaremill.session.{GetSessionTransport, SetSessionTransport} -import app.softnetwork.session.{TapirCsrfCheckMode, TapirEndpoints, TapirSessionContinuity} -import org.softnetwork.session.model.Session -import sttp.model.headers.WWWAuthenticateChallenge -import sttp.tapir._ -import sttp.tapir.model.UsernamePassword -import sttp.tapir.server.{PartialServerEndpointWithSecurityOutput, ServerEndpoint} - -import scala.concurrent.{ExecutionContext, Future} -import scala.language.implicitConversions - -trait BasicServiceEndpoint extends ApiEndpoint with TapirEndpoints { - - def sessionEndpoints: SessionEndpoints - - def sc: TapirSessionContinuity[Session] = sessionEndpoints.sc - - def st: SetSessionTransport = sessionEndpoints.st - - def gt: GetSessionTransport = sessionEndpoints.gt - - def checkMode: TapirCsrfCheckMode[Session] = sessionEndpoints.checkMode - - implicit def usernamePassword2Session(credentials: UsernamePassword): Option[Session] = { - Some(Session(credentials.username)) - } - - val emptySecurityEndpoint - : PartialServerEndpointWithSecurityOutput[Unit, Unit, Unit, Unit, Unit, Unit, Any, Future] = - endpoint.serverSecurityLogicSuccessWithOutput(_ => Future.successful(((), ()))) - - val createSessionEndpoint: ServerEndpoint[Any, Future] = { - hmacTokenCsrfProtection(checkMode) { - setSessionWithAuth(sc, st)( - auth.basic[UsernamePassword](WWWAuthenticateChallenge.basic("Basic Realm")) - ) - }.post - .in("auth" / "basic") - .serverLogicSuccess(_ => _ => Future.successful(())) - } - - val invalidateSessionEndpoint: ServerEndpoint[Any, Future] = - sessionEndpoints - .invalidateSession(sc, st) { - emptySecurityEndpoint - } - .delete - .in("auth" / "basic") - .serverLogicSuccess(_ => _ => Future.successful(())) - - override def endpoints: List[ServerEndpoint[Any, Future]] = - List(createSessionEndpoint, invalidateSessionEndpoint) - -} - -object BasicServiceEndpoint { - def apply(_system: ActorSystem[_], _sessionEndpoints: SessionEndpoints): BasicServiceEndpoint = - new BasicServiceEndpoint { - override def sessionEndpoints: SessionEndpoints = _sessionEndpoints - override implicit def ec: ExecutionContext = _system.executionContext - } -} diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/BasicServiceRoute.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/BasicServiceRoute.scala deleted file mode 100644 index b63d848..0000000 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/BasicServiceRoute.scala +++ /dev/null @@ -1,81 +0,0 @@ -package app.softnetwork.payment.api - -import akka.actor.typed.ActorSystem -import akka.http.scaladsl.model.{HttpResponse, StatusCodes} -import akka.http.scaladsl.server.{Directives, Route} -import akka.http.scaladsl.server.directives.Credentials -import app.softnetwork.api.server.DefaultComplete -import app.softnetwork.session.service.SessionService -import com.softwaremill.session.CsrfDirectives.{hmacTokenCsrfProtection, setNewCsrfToken} -import com.softwaremill.session.CsrfOptions.checkHeader -import de.heikoseeberger.akkahttpjson4s.Json4sSupport -import org.json4s.Formats -import org.softnetwork.session.model.Session - -trait BasicServiceRoute extends Directives with DefaultComplete with Json4sSupport { - - import app.softnetwork.persistence.generateUUID - import app.softnetwork.serialization._ - - import Session._ - - implicit def formats: Formats = commonFormats - - def sessionService: SessionService - - val route: Route = { - pathPrefix("auth") { - basic - } - } - - lazy val basic: Route = path("basic") { - get { - // check anti CSRF token - hmacTokenCsrfProtection(checkHeader) { - // check if a session exists - sessionService.requiredSession { session => - complete(HttpResponse(StatusCodes.OK, entity = session.id)) - } - } - } ~ post { - authenticateBasic("Basic Realm", BasicAuthAuthenticator) { identifier => - // create a new session - val session = Session(generateUUID(identifier)) - sessionService.setSession(session) { - // create a new anti csrf token - setNewCsrfToken(checkHeader) { - complete(HttpResponse(StatusCodes.OK, entity = session.id)) - } - } - } - } ~ delete { - // check anti CSRF token - hmacTokenCsrfProtection(checkHeader) { - // check if a session exists - sessionService.requiredSession { _ => - // invalidate session - sessionService.invalidateSession { - complete(HttpResponse(StatusCodes.OK)) - } - } - } - } - } - - private def BasicAuthAuthenticator(credentials: Credentials): Option[String] = { - credentials match { - case p @ Credentials.Provided(_) => Some(p.identifier) - case _ => None - } - } - -} - -object BasicServiceRoute { - def apply(_system: ActorSystem[_], _sessionService: SessionService): BasicServiceRoute = { - new BasicServiceRoute { - override def sessionService: SessionService = _sessionService - } - } -} diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayApi.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayApi.scala index 805ce0f..7fa29f8 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayApi.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayApi.scala @@ -1,64 +1,144 @@ package app.softnetwork.payment.api import akka.actor.typed.ActorSystem -import akka.http.scaladsl.model.{HttpRequest, HttpResponse} +import app.softnetwork.account.config.AccountSettings +import app.softnetwork.account.persistence.query.AccountEventProcessorStreams.InternalAccountEvents2AccountProcessorStream import app.softnetwork.api.server.SwaggerEndpoint -import app.softnetwork.payment.handlers.MangoPayPaymentHandler +import app.softnetwork.payment.config.PaymentSettings +import app.softnetwork.payment.handlers.{PaymentHandler, SoftPayAccountTypeKey} import app.softnetwork.payment.launch.PaymentApplication import app.softnetwork.payment.persistence.query.{ - GenericPaymentCommandProcessorStream, + PaymentCommandProcessorStream, Scheduler2PaymentProcessorStream } -import app.softnetwork.payment.persistence.typed.{GenericPaymentBehavior, MangoPayPaymentBehavior} -import app.softnetwork.payment.service.MangoPayPaymentEndpoints +import app.softnetwork.payment.persistence.typed.{PaymentBehavior, SoftPayAccountBehavior} +import app.softnetwork.payment.service.{ + MangoPayPaymentServiceEndpoints, + SoftPayAccountServiceEndpoints, + SoftPayOAuthServiceEndpoints +} import app.softnetwork.persistence.jdbc.query.{JdbcJournalProvider, JdbcOffsetProvider} import app.softnetwork.persistence.schema.SchemaProvider import app.softnetwork.scheduler.config.SchedulerSettings import app.softnetwork.session.CsrfCheck -import app.softnetwork.session.service.SessionEndpoints +import app.softnetwork.session.config.Settings +import app.softnetwork.session.model.{ + SessionData, + SessionDataCompanion, + SessionDataDecorator, + SessionManagers +} +import app.softnetwork.session.service.SessionMaterials +import com.softwaremill.session.{RefreshTokenStorage, SessionConfig, SessionManager} import com.typesafe.config.Config import org.slf4j.{Logger, LoggerFactory} +import org.softnetwork.session.model.Session +import sttp.tapir.swagger.SwaggerUIOptions -import scala.concurrent.Future +import scala.concurrent.ExecutionContext -trait MangoPayApi extends PaymentApplication { self: SchemaProvider with CsrfCheck => +trait MangoPayApi[SD <: SessionData with SessionDataDecorator[SD]] extends PaymentApplication { + self: SchemaProvider with CsrfCheck => - override def paymentAccountBehavior: ActorSystem[_] => GenericPaymentBehavior = _ => - MangoPayPaymentBehavior - - override def paymentCommandProcessorStream - : ActorSystem[_] => GenericPaymentCommandProcessorStream = sys => - new GenericPaymentCommandProcessorStream - with MangoPayPaymentHandler + override def internalAccountEvents2AccountProcessorStream + : ActorSystem[_] => InternalAccountEvents2AccountProcessorStream = sys => + new InternalAccountEvents2AccountProcessorStream + with SoftPayAccountTypeKey with JdbcJournalProvider with JdbcOffsetProvider { override def config: Config = MangoPayApi.this.config + override def tag: String = s"${SoftPayAccountBehavior.persistenceId}-to-internal" override implicit def system: ActorSystem[_] = sys } + override def paymentCommandProcessorStream: ActorSystem[_] => PaymentCommandProcessorStream = + sys => + new PaymentCommandProcessorStream + with PaymentHandler + with JdbcJournalProvider + with JdbcOffsetProvider { + override def config: Config = MangoPayApi.this.config + override implicit def system: ActorSystem[_] = sys + } + override def scheduler2PaymentProcessorStream : ActorSystem[_] => Scheduler2PaymentProcessorStream = sys => new Scheduler2PaymentProcessorStream - with MangoPayPaymentHandler + with PaymentHandler with JdbcJournalProvider with JdbcOffsetProvider { override def config: Config = MangoPayApi.this.config - override val tag: String = SchedulerSettings.tag(MangoPayPaymentBehavior.persistenceId) + override val tag: String = SchedulerSettings.tag(PaymentBehavior.persistenceId) override implicit def system: ActorSystem[_] = sys } - override def grpcServices - : ActorSystem[_] => Seq[PartialFunction[HttpRequest, Future[HttpResponse]]] = - system => - Seq( - PaymentServiceApiHandler.partial(MangoPayServer(system))(system) - ) + implicit def sessionConfig: SessionConfig = Settings.Session.DefaultSessionConfig + + implicit def companion: SessionDataCompanion[SD] + + protected def manager: SessionManager[SD] = SessionManagers.basic + + protected def refreshTokenStorage: ActorSystem[_] => RefreshTokenStorage[SD] + + override protected def sessionType: Session.SessionType = + Settings.Session.SessionContinuityAndTransport def paymentSwagger: ActorSystem[_] => SwaggerEndpoint = sys => - new MangoPayPaymentEndpoints with SwaggerEndpoint { + new MangoPayPaymentServiceEndpoints[SD] with SwaggerEndpoint with SessionMaterials[SD] { + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected def sessionType: Session.SessionType = self.sessionType + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def sessionConfig: SessionConfig = self.sessionConfig override implicit def system: ActorSystem[_] = sys - lazy val log: Logger = LoggerFactory getLogger getClass.getName - override def sessionEndpoints: SessionEndpoints = self.sessionEndpoints(system) + override lazy val ec: ExecutionContext = sys.executionContext + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[SD] = self.companion override val applicationVersion: String = systemVersion() + override val swaggerUIOptions: SwaggerUIOptions = + SwaggerUIOptions.default.pathPrefix(List("swagger", PaymentSettings.PaymentPath)) } + + def accountSwagger: ActorSystem[_] => SwaggerEndpoint = sys => + new SoftPayAccountServiceEndpoints[SD] with SwaggerEndpoint with SessionMaterials[SD] { + lazy val log: Logger = LoggerFactory getLogger getClass.getName + override implicit def system: ActorSystem[_] = sys + override implicit lazy val ec: ExecutionContext = sys.executionContext + override protected def sessionType: Session.SessionType = self.sessionType + override implicit def sessionConfig: SessionConfig = self.sessionConfig + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected val manifestWrapper: ManifestW = ManifestW() + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[SD] = self.companion + override val applicationVersion: String = self.systemVersion() + override val swaggerUIOptions: SwaggerUIOptions = + SwaggerUIOptions.default.pathPrefix(List("swagger", AccountSettings.Path)) + } + + def oauthSwagger: ActorSystem[_] => SwaggerEndpoint = + sys => + new SoftPayOAuthServiceEndpoints[SD] with SwaggerEndpoint with SessionMaterials[SD] { + override implicit def system: ActorSystem[_] = sys + override implicit lazy val ec: ExecutionContext = sys.executionContext + override protected def sessionType: Session.SessionType = self.sessionType + override implicit def sessionConfig: SessionConfig = self.sessionConfig + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[SD] = self.companion + override val applicationVersion: String = self.systemVersion() + override val swaggerUIOptions: SwaggerUIOptions = + SwaggerUIOptions.default.pathPrefix(List("swagger", AccountSettings.OAuthPath)) + } } diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayEndpoints.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayEndpoints.scala index 5ed5636..b9b9bb7 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayEndpoints.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayEndpoints.scala @@ -1,17 +1,81 @@ package app.softnetwork.payment.api import akka.actor.typed.ActorSystem +import app.softnetwork.account.message +import app.softnetwork.account.service.{AccountServiceEndpoints, OAuthServiceEndpoints} import app.softnetwork.api.server.Endpoint import app.softnetwork.payment.launch.PaymentEndpoints -import app.softnetwork.payment.service.{GenericPaymentEndpoints, MangoPayPaymentEndpoints} +import app.softnetwork.payment.service.{ + MangoPayPaymentServiceEndpoints, + PaymentServiceEndpoints, + SoftPayAccountServiceEndpoints, + SoftPayOAuthServiceEndpoints +} import app.softnetwork.persistence.schema.SchemaProvider import app.softnetwork.session.CsrfCheck +import app.softnetwork.session.model.{SessionData, SessionDataCompanion, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials +import com.softwaremill.session.{RefreshTokenStorage, SessionConfig, SessionManager} +import org.softnetwork.session.model.Session + +import scala.concurrent.ExecutionContext + +trait MangoPayEndpoints[SD <: SessionData with SessionDataDecorator[SD]] + extends PaymentEndpoints[SD] { + self: MangoPayApi[SD] with SchemaProvider with CsrfCheck => + + override def paymentEndpoints: ActorSystem[_] => PaymentServiceEndpoints[SD] = sys => + new MangoPayPaymentServiceEndpoints[SD] with SessionMaterials[SD] { + override def log: org.slf4j.Logger = org.slf4j.LoggerFactory.getLogger(getClass) + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected def sessionType: Session.SessionType = self.sessionType + override implicit def sessionConfig: SessionConfig = self.sessionConfig + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[SD] = self.companion + } + + override def accountEndpoints + : ActorSystem[_] => AccountServiceEndpoints[message.BasicAccountSignUp, SD] = sys => + new SoftPayAccountServiceEndpoints[SD] with SessionMaterials[SD] { + override def log: org.slf4j.Logger = org.slf4j.LoggerFactory.getLogger(getClass) + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected def sessionType: Session.SessionType = self.sessionType + override implicit def sessionConfig: SessionConfig = self.sessionConfig + override protected val manifestWrapper: ManifestW = ManifestW() + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[SD] = self.companion + } -trait MangoPayEndpoints extends PaymentEndpoints { - _: MangoPayApi with SchemaProvider with CsrfCheck => - override def paymentEndpoints: ActorSystem[_] => GenericPaymentEndpoints = system => - MangoPayPaymentEndpoints(system, sessionEndpoints(system)) + override def oauthEndpoints: ActorSystem[_] => OAuthServiceEndpoints[SD] = sys => + new SoftPayOAuthServiceEndpoints[SD] with SessionMaterials[SD] { + override def log: org.slf4j.Logger = org.slf4j.LoggerFactory.getLogger(getClass) + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected def sessionType: Session.SessionType = self.sessionType + override implicit def sessionConfig: SessionConfig = self.sessionConfig + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[SD] = self.companion + } override def endpoints: ActorSystem[_] => List[Endpoint] = system => - super.endpoints(system) :+ paymentSwagger(system) + super.endpoints(system) :+ paymentSwagger(system) :+ accountSwagger(system) :+ oauthSwagger( + system + ) } diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayEndpointsApi.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayEndpointsApi.scala index a1ea704..b3ad203 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayEndpointsApi.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayEndpointsApi.scala @@ -2,8 +2,11 @@ package app.softnetwork.payment.api import app.softnetwork.persistence.schema.SchemaProvider import app.softnetwork.session.CsrfCheck +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} -trait MangoPayEndpointsApi extends MangoPayApi with MangoPayEndpoints { +trait MangoPayEndpointsApi[SD <: SessionData with SessionDataDecorator[SD]] + extends MangoPayApi[SD] + with MangoPayEndpoints[SD] { _: SchemaProvider with CsrfCheck => } diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayEndpointsPostgresLauncher.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayEndpointsPostgresLauncher.scala index 5f446fc..c1f040b 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayEndpointsPostgresLauncher.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayEndpointsPostgresLauncher.scala @@ -1,17 +1,45 @@ package app.softnetwork.payment.api +import akka.actor.typed.ActorSystem +import app.softnetwork.payment.handlers.SoftPayAccountDao +import app.softnetwork.payment.service.{MangoPayPaymentServiceEndpoints, PaymentServiceEndpoints} import app.softnetwork.persistence.jdbc.schema.{JdbcSchemaProvider, JdbcSchemaTypes} import app.softnetwork.persistence.schema.SchemaType import app.softnetwork.session.CsrfCheckHeader +import app.softnetwork.session.handlers.JwtClaimsRefreshTokenDao +import app.softnetwork.session.model.{SessionDataCompanion, SessionManagers} +import app.softnetwork.session.service.JwtClaimsSessionMaterials +import com.softwaremill.session.{RefreshTokenStorage, SessionConfig, SessionManager} import org.slf4j.{Logger, LoggerFactory} +import org.softnetwork.session.model.{ApiKey, JwtClaims, Session} + +import scala.concurrent.{ExecutionContext, Future} object MangoPayEndpointsPostgresLauncher - extends MangoPayEndpointsApi + extends MangoPayEndpointsApi[JwtClaims] with JdbcSchemaProvider - with CsrfCheckHeader { + with CsrfCheckHeader { self => lazy val log: Logger = LoggerFactory getLogger getClass.getName override def schemaType: SchemaType = JdbcSchemaTypes.Postgres + override implicit def companion: SessionDataCompanion[JwtClaims] = JwtClaims + + override protected def refreshTokenStorage: ActorSystem[_] => RefreshTokenStorage[JwtClaims] = + sys => JwtClaimsRefreshTokenDao(sys) + + override protected def manager: SessionManager[JwtClaims] = SessionManagers.jwt + + override def paymentEndpoints: ActorSystem[_] => PaymentServiceEndpoints[JwtClaims] = sys => + new MangoPayPaymentServiceEndpoints[JwtClaims] with JwtClaimsSessionMaterials { + override implicit def system: ActorSystem[_] = sys + override implicit lazy val ec: ExecutionContext = sys.executionContext + override implicit def sessionConfig: SessionConfig = self.sessionConfig + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def refreshTokenStorage: RefreshTokenStorage[JwtClaims] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[JwtClaims] = self.companion + override protected def sessionType: Session.SessionType = self.sessionType + } } diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayRoutes.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayRoutes.scala index a9ddf62..98ce1af 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayRoutes.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayRoutes.scala @@ -1,16 +1,87 @@ package app.softnetwork.payment.api import akka.actor.typed.ActorSystem +import app.softnetwork.account.model.{ + DefaultAccountDetailsView, + DefaultAccountView, + DefaultProfileView +} +import app.softnetwork.account.service.{AccountService, OAuthService} import app.softnetwork.api.server.ApiRoute import app.softnetwork.payment.launch.PaymentRoutes -import app.softnetwork.payment.service.{GenericPaymentService, MangoPayPaymentService} +import app.softnetwork.payment.service.{ + MangoPayPaymentService, + PaymentService, + SoftPayAccountService, + SoftPayOAuthService +} import app.softnetwork.persistence.schema.SchemaProvider +import app.softnetwork.session.CsrfCheck +import app.softnetwork.session.model.{SessionData, SessionDataCompanion, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials +import com.softwaremill.session.{RefreshTokenStorage, SessionConfig, SessionManager} +import org.slf4j.{Logger, LoggerFactory} +import org.softnetwork.session.model.Session + +import scala.concurrent.ExecutionContext + +trait MangoPayRoutes[SD <: SessionData with SessionDataDecorator[SD]] extends PaymentRoutes[SD] { + self: MangoPayApi[SD] with SchemaProvider with CsrfCheck => + + override def paymentService: ActorSystem[_] => PaymentService[SD] = sys => + new MangoPayPaymentService[SD] with SessionMaterials[SD] { + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected def sessionType: Session.SessionType = self.sessionType + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def sessionConfig: SessionConfig = self.sessionConfig + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[SD] = self.companion + } -trait MangoPayRoutes extends PaymentRoutes { _: MangoPayApi with SchemaProvider => + override def accountService: ActorSystem[_] => AccountService[ + DefaultProfileView, + DefaultAccountDetailsView, + DefaultAccountView[DefaultProfileView, DefaultAccountDetailsView], + SD + ] = sys => + new SoftPayAccountService[SD] with SessionMaterials[SD] { + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected def sessionType: Session.SessionType = self.sessionType + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + override protected val manifestWrapper: ManifestW = ManifestW() + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[SD] = self.companion + } - override def paymentService: ActorSystem[_] => GenericPaymentService = system => - MangoPayPaymentService(system, sessionService(system)) + override def oauthService: ActorSystem[_] => OAuthService[SD] = sys => + new SoftPayOAuthService[SD] with SessionMaterials[SD] { + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected def sessionType: Session.SessionType = self.sessionType + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[SD] = self.companion + } override def apiRoutes: ActorSystem[_] => List[ApiRoute] = system => - super.apiRoutes(system) :+ paymentSwagger(system) + super.apiRoutes(system) :+ paymentSwagger(system) :+ accountSwagger(system) :+ oauthSwagger( + system + ) } diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayRoutesApi.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayRoutesApi.scala index bd58cfc..44f5a62 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayRoutesApi.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayRoutesApi.scala @@ -2,7 +2,10 @@ package app.softnetwork.payment.api import app.softnetwork.persistence.schema.SchemaProvider import app.softnetwork.session.CsrfCheck +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} -trait MangoPayRoutesApi extends MangoPayApi with MangoPayRoutes { +trait MangoPayRoutesApi[SD <: SessionData with SessionDataDecorator[SD]] + extends MangoPayApi[SD] + with MangoPayRoutes[SD] { _: SchemaProvider with CsrfCheck => } diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayRoutesPostgresLauncher.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayRoutesPostgresLauncher.scala index cb2bc1d..80d9828 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayRoutesPostgresLauncher.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayRoutesPostgresLauncher.scala @@ -1,16 +1,43 @@ package app.softnetwork.payment.api +import akka.actor.typed.ActorSystem +import app.softnetwork.payment.service.{MangoPayPaymentService, PaymentService} import app.softnetwork.persistence.jdbc.schema.{JdbcSchemaProvider, JdbcSchemaTypes} import app.softnetwork.persistence.schema.SchemaType import app.softnetwork.session.CsrfCheckHeader +import app.softnetwork.session.handlers.JwtClaimsRefreshTokenDao +import app.softnetwork.session.model.{SessionDataCompanion, SessionManagers} +import app.softnetwork.session.service.JwtClaimsSessionMaterials +import com.softwaremill.session.{RefreshTokenStorage, SessionConfig, SessionManager} import org.slf4j.{Logger, LoggerFactory} +import org.softnetwork.session.model.{JwtClaims, Session} + +import scala.concurrent.ExecutionContext object MangoPayRoutesPostgresLauncher - extends MangoPayRoutesApi + extends MangoPayRoutesApi[JwtClaims] with JdbcSchemaProvider - with CsrfCheckHeader { + with CsrfCheckHeader { self => lazy val log: Logger = LoggerFactory getLogger getClass.getName override def schemaType: SchemaType = JdbcSchemaTypes.Postgres + override implicit def companion: SessionDataCompanion[JwtClaims] = JwtClaims + + override protected def refreshTokenStorage: ActorSystem[_] => RefreshTokenStorage[JwtClaims] = + sys => JwtClaimsRefreshTokenDao(sys) + + override protected def manager: SessionManager[JwtClaims] = SessionManagers.jwt + + override def paymentService: ActorSystem[_] => PaymentService[JwtClaims] = sys => + new MangoPayPaymentService[JwtClaims] with JwtClaimsSessionMaterials { + override implicit def system: ActorSystem[_] = sys + override implicit lazy val ec: ExecutionContext = sys.executionContext + override implicit def sessionConfig: SessionConfig = self.sessionConfig + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def refreshTokenStorage: RefreshTokenStorage[JwtClaims] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[JwtClaims] = self.companion + override protected def sessionType: Session.SessionType = self.sessionType + } } diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerApi.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerApi.scala index 10e8059..8346d64 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerApi.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerApi.scala @@ -1,19 +1,20 @@ package app.softnetwork.payment.api import akka.actor.typed.ActorSystem -import akka.http.scaladsl.model.{HttpRequest, HttpResponse} +import app.softnetwork.api.server.GrpcService import app.softnetwork.persistence.jdbc.query.{JdbcJournalProvider, JdbcOffsetProvider} import app.softnetwork.persistence.launch.PersistentEntity import app.softnetwork.persistence.query.EventProcessorStream import app.softnetwork.persistence.schema.SchemaProvider -import app.softnetwork.scheduler.api.{SchedulerApi, SchedulerServiceApiHandler} +import app.softnetwork.scheduler.api.SchedulerApi import app.softnetwork.scheduler.handlers.SchedulerHandler import app.softnetwork.scheduler.persistence.query.Entity2SchedulerProcessorStream +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} import com.typesafe.config.Config -import scala.concurrent.Future - -trait MangoPayWithSchedulerApi extends MangoPayApi with SchedulerApi { _: SchemaProvider => +trait MangoPayWithSchedulerApi[SD <: SessionData with SessionDataDecorator[SD]] + extends MangoPayApi[SD] + with SchedulerApi { _: SchemaProvider => override def entity2SchedulerProcessorStream: ActorSystem[_] => Entity2SchedulerProcessorStream = sys => @@ -36,9 +37,6 @@ trait MangoPayWithSchedulerApi extends MangoPayApi with SchedulerApi { _: Schema initSchedulerSystem(system) }*/ - override def grpcServices - : ActorSystem[_] => Seq[PartialFunction[HttpRequest, Future[HttpResponse]]] = system => - Seq( - PaymentServiceApiHandler.partial(MangoPayServer(system))(system) - ) :+ SchedulerServiceApiHandler.partial(schedulerServer(system))(system) + override def grpcServices: ActorSystem[_] => Seq[GrpcService] = system => + paymentGrpcServices(system) ++ schedulerGrpcServices(system) } diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerEndpoints.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerEndpoints.scala index 86c4665..7de1873 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerEndpoints.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerEndpoints.scala @@ -1,14 +1,44 @@ package app.softnetwork.payment.api import akka.actor.typed.ActorSystem -import app.softnetwork.api.server.Endpoint +import app.softnetwork.api.server.{Endpoint, SwaggerApiEndpoint} import app.softnetwork.persistence.schema.SchemaProvider +import app.softnetwork.scheduler.config.SchedulerSettings import app.softnetwork.scheduler.launch.SchedulerEndpoints +import app.softnetwork.scheduler.service.SchedulerServiceEndpoints import app.softnetwork.session.CsrfCheck +import app.softnetwork.session.model.{SessionData, SessionDataCompanion, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials +import com.softwaremill.session.{RefreshTokenStorage, SessionConfig, SessionManager} +import org.slf4j.{Logger, LoggerFactory} +import org.softnetwork.session.model.Session +import sttp.tapir.swagger.SwaggerUIOptions -trait MangoPayWithSchedulerEndpoints extends SchedulerEndpoints with MangoPayEndpoints { - _: MangoPayWithSchedulerApi with SchemaProvider with CsrfCheck => +import scala.concurrent.ExecutionContext + +trait MangoPayWithSchedulerEndpoints[SD <: SessionData with SessionDataDecorator[SD]] + extends SchedulerEndpoints[SD] + with MangoPayEndpoints[SD] { + self: MangoPayWithSchedulerApi[SD] with SchemaProvider with CsrfCheck => + + override def schedulerEndpoints: ActorSystem[_] => SchedulerServiceEndpoints[SD] = sys => + new SchedulerServiceEndpoints[SD] with SwaggerApiEndpoint with SessionMaterials[SD] { + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + lazy val log: Logger = LoggerFactory getLogger getClass.getName + override protected def sessionType: Session.SessionType = self.sessionType + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[SD] = self.companion + override val applicationVersion: String = systemVersion() + override val swaggerUIOptions: SwaggerUIOptions = + SwaggerUIOptions.default.pathPrefix(List("swagger", SchedulerSettings.SchedulerPath)) + } override def endpoints: ActorSystem[_] => List[Endpoint] = system => - super.endpoints(system) :+ schedulerEndpoints(system) :+ schedulerSwagger(system) + super.endpoints(system) :+ schedulerEndpoints(system) } diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerEndpointsApi.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerEndpointsApi.scala index f254567..ca5794c 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerEndpointsApi.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerEndpointsApi.scala @@ -2,7 +2,8 @@ package app.softnetwork.payment.api import app.softnetwork.persistence.schema.SchemaProvider import app.softnetwork.session.CsrfCheck +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} -trait MangoPayWithSchedulerEndpointsApi - extends MangoPayWithSchedulerApi - with MangoPayWithSchedulerEndpoints { _: SchemaProvider with CsrfCheck => } +trait MangoPayWithSchedulerEndpointsApi[SD <: SessionData with SessionDataDecorator[SD]] + extends MangoPayWithSchedulerApi[SD] + with MangoPayWithSchedulerEndpoints[SD] { _: SchemaProvider with CsrfCheck => } diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerEndpointsPostgresLauncher.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerEndpointsPostgresLauncher.scala index 6c8bca7..a3c5144 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerEndpointsPostgresLauncher.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerEndpointsPostgresLauncher.scala @@ -1,17 +1,45 @@ package app.softnetwork.payment.api +import akka.actor.typed.ActorSystem +import app.softnetwork.payment.handlers.SoftPayAccountDao +import app.softnetwork.payment.service.{MangoPayPaymentServiceEndpoints, PaymentServiceEndpoints} import app.softnetwork.persistence.jdbc.schema.{JdbcSchemaProvider, JdbcSchemaTypes} import app.softnetwork.persistence.schema.SchemaType import app.softnetwork.session.CsrfCheckHeader +import app.softnetwork.session.handlers.JwtClaimsRefreshTokenDao +import app.softnetwork.session.model.{SessionDataCompanion, SessionManagers} +import app.softnetwork.session.service.JwtClaimsSessionMaterials +import com.softwaremill.session.{RefreshTokenStorage, SessionConfig, SessionManager} import org.slf4j.{Logger, LoggerFactory} +import org.softnetwork.session.model.{ApiKey, JwtClaims, Session} + +import scala.concurrent.{ExecutionContext, Future} object MangoPayWithSchedulerEndpointsPostgresLauncher - extends MangoPayWithSchedulerEndpointsApi + extends MangoPayWithSchedulerEndpointsApi[JwtClaims] with JdbcSchemaProvider - with CsrfCheckHeader { + with CsrfCheckHeader { self => lazy val log: Logger = LoggerFactory getLogger getClass.getName override def schemaType: SchemaType = JdbcSchemaTypes.Postgres + override implicit def companion: SessionDataCompanion[JwtClaims] = JwtClaims + + override protected def refreshTokenStorage: ActorSystem[_] => RefreshTokenStorage[JwtClaims] = + sys => JwtClaimsRefreshTokenDao(sys) + + override protected def manager: SessionManager[JwtClaims] = SessionManagers.jwt + + override def paymentEndpoints: ActorSystem[_] => PaymentServiceEndpoints[JwtClaims] = sys => + new MangoPayPaymentServiceEndpoints[JwtClaims] with JwtClaimsSessionMaterials { + override implicit def system: ActorSystem[_] = sys + override implicit lazy val ec: ExecutionContext = sys.executionContext + override implicit def sessionConfig: SessionConfig = self.sessionConfig + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def refreshTokenStorage: RefreshTokenStorage[JwtClaims] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[JwtClaims] = self.companion + override protected def sessionType: Session.SessionType = self.sessionType + } } diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerRoutes.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerRoutes.scala index 6f96b05..0734eb2 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerRoutes.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerRoutes.scala @@ -1,12 +1,59 @@ package app.softnetwork.payment.api import akka.actor.typed.ActorSystem -import app.softnetwork.api.server.ApiRoute +import app.softnetwork.api.server.{ApiRoute, SwaggerEndpoint} import app.softnetwork.persistence.schema.SchemaProvider +import app.softnetwork.scheduler.config.SchedulerSettings import app.softnetwork.scheduler.launch.SchedulerRoutes +import app.softnetwork.scheduler.service.{SchedulerService, SchedulerServiceEndpoints} +import app.softnetwork.session.model.{SessionData, SessionDataCompanion, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials +import com.softwaremill.session.{RefreshTokenStorage, SessionConfig, SessionManager} +import org.slf4j.{Logger, LoggerFactory} +import org.softnetwork.session.model.Session +import sttp.tapir.swagger.SwaggerUIOptions + +import scala.concurrent.ExecutionContext + +trait MangoPayWithSchedulerRoutes[SD <: SessionData with SessionDataDecorator[SD]] + extends SchedulerRoutes[SD] + with MangoPayRoutes[SD] { + self: MangoPayWithSchedulerApi[SD] with SchemaProvider => + + override def schedulerService: ActorSystem[_] => SchedulerService[SD] = sys => + new SchedulerService[SD] with SessionMaterials[SD] { + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + lazy val log: Logger = LoggerFactory getLogger getClass.getName + override protected def sessionType: Session.SessionType = self.sessionType + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[SD] = self.companion + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + } + + private def schedulerSwagger: ActorSystem[_] => SwaggerEndpoint = + sys => + new SchedulerServiceEndpoints[SD] with SwaggerEndpoint with SessionMaterials[SD] { + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + lazy val log: Logger = LoggerFactory getLogger getClass.getName + override protected def sessionType: Session.SessionType = self.sessionType + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[SD] = self.companion + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override val applicationVersion: String = systemVersion() + override val swaggerUIOptions: SwaggerUIOptions = + SwaggerUIOptions.default.pathPrefix(List("swagger", SchedulerSettings.SchedulerPath)) + } -trait MangoPayWithSchedulerRoutes extends SchedulerRoutes with MangoPayRoutes { - _: MangoPayWithSchedulerApi with SchemaProvider => override def apiRoutes: ActorSystem[_] => List[ApiRoute] = system => super.apiRoutes(system) :+ schedulerService(system) :+ schedulerSwagger(system) } diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerRoutesApi.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerRoutesApi.scala index 7acc945..c124e03 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerRoutesApi.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerRoutesApi.scala @@ -1,7 +1,8 @@ package app.softnetwork.payment.api import app.softnetwork.persistence.schema.SchemaProvider +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} -trait MangoPayWithSchedulerRoutesApi - extends MangoPayWithSchedulerApi - with MangoPayWithSchedulerRoutes { _: SchemaProvider => } +trait MangoPayWithSchedulerRoutesApi[SD <: SessionData with SessionDataDecorator[SD]] + extends MangoPayWithSchedulerApi[SD] + with MangoPayWithSchedulerRoutes[SD] { _: SchemaProvider => } diff --git a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerRoutesPostgresLauncher.scala b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerRoutesPostgresLauncher.scala index 34c70e7..c65b4ac 100644 --- a/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerRoutesPostgresLauncher.scala +++ b/mangopay/api/src/main/scala/app/softnetwork/payment/api/MangoPayWithSchedulerRoutesPostgresLauncher.scala @@ -1,14 +1,42 @@ package app.softnetwork.payment.api +import akka.actor.typed.ActorSystem +import app.softnetwork.payment.handlers.SoftPayAccountDao +import app.softnetwork.payment.service.{MangoPayPaymentService, PaymentService} import app.softnetwork.persistence.jdbc.schema.{JdbcSchemaProvider, JdbcSchemaTypes} import app.softnetwork.persistence.schema.SchemaType +import app.softnetwork.session.handlers.JwtClaimsRefreshTokenDao +import app.softnetwork.session.model.{SessionDataCompanion, SessionManagers} +import app.softnetwork.session.service.JwtClaimsSessionMaterials +import com.softwaremill.session.{RefreshTokenStorage, SessionConfig, SessionManager} import org.slf4j.{Logger, LoggerFactory} +import org.softnetwork.session.model.{ApiKey, JwtClaims, Session} + +import scala.concurrent.{ExecutionContext, Future} object MangoPayWithSchedulerRoutesPostgresLauncher - extends MangoPayWithSchedulerRoutesApi - with JdbcSchemaProvider { + extends MangoPayWithSchedulerRoutesApi[JwtClaims] + with JdbcSchemaProvider { self => lazy val log: Logger = LoggerFactory getLogger getClass.getName override def schemaType: SchemaType = JdbcSchemaTypes.Postgres + override implicit def companion: SessionDataCompanion[JwtClaims] = JwtClaims + + override protected def refreshTokenStorage: ActorSystem[_] => RefreshTokenStorage[JwtClaims] = + sys => JwtClaimsRefreshTokenDao(sys) + + override protected def manager: SessionManager[JwtClaims] = SessionManagers.jwt + + override def paymentService: ActorSystem[_] => PaymentService[JwtClaims] = sys => + new MangoPayPaymentService[JwtClaims] with JwtClaimsSessionMaterials { + override implicit def system: ActorSystem[_] = sys + override implicit lazy val ec: ExecutionContext = sys.executionContext + override implicit def sessionConfig: SessionConfig = self.sessionConfig + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def refreshTokenStorage: RefreshTokenStorage[JwtClaims] = + self.refreshTokenStorage(sys) + override implicit def companion: SessionDataCompanion[JwtClaims] = self.companion + override protected def sessionType: Session.SessionType = self.sessionType + } } diff --git a/mangopay/src/main/resources/META-INF/services/app.softnetwork.payment.spi.PaymentProviderSpi b/mangopay/src/main/resources/META-INF/services/app.softnetwork.payment.spi.PaymentProviderSpi new file mode 100644 index 0000000..9a9e11c --- /dev/null +++ b/mangopay/src/main/resources/META-INF/services/app.softnetwork.payment.spi.PaymentProviderSpi @@ -0,0 +1 @@ +app.softnetwork.payment.spi.MangoPayProviderFactory diff --git a/mangopay/src/main/resources/reference.conf b/mangopay/src/main/resources/reference.conf index e8a5cff..9c1552b 100644 --- a/mangopay/src/main/resources/reference.conf +++ b/mangopay/src/main/resources/reference.conf @@ -1,11 +1,8 @@ payment{ - path = "payment" - path = ${?PAYMENT_PATH} - mangopay { version = "2.01" - client-id = "softnetworktest" - api-key = "qEdjqnNwoTUOMJzQcWEB7CH0nokvkJe0Vy83dxtBQ3fR1SRzq7" + client-id = "softpaymentdev" + api-key = "aJLYtPbdE672E7HHNk6jZrmz9Mwo8YWs05KZXVXje7xKWZHa8j" baseUrl = "https://api.sandbox.mangopay.com/" debug = false # https://docs.mangopay.com/guide/errors diff --git a/mangopay/src/main/scala/app/softnetwork/payment/config/MangoPay.scala b/mangopay/src/main/scala/app/softnetwork/payment/config/MangoPay.scala index bd174b6..3e844ff 100644 --- a/mangopay/src/main/scala/app/softnetwork/payment/config/MangoPay.scala +++ b/mangopay/src/main/scala/app/softnetwork/payment/config/MangoPay.scala @@ -1,10 +1,10 @@ package app.softnetwork.payment.config import MangoPaySettings._ +import app.softnetwork.payment.model.SoftPayAccount import com.mangopay.MangoPayApi import com.mangopay.core.enumerations.{EventType, HookStatus} import com.mangopay.entities.Hook - import com.typesafe.scalalogging.StrictLogging import scala.util.{Failure, Success, Try} @@ -41,18 +41,24 @@ object MangoPay extends StrictLogging { lazy val payPalReturnUrl = s"""$BaseUrl/$paypalPath/$PayPalRoute""" } - var maybeMangoPayApi: Option[MangoPayApi] = None + var mangoPayApis: Map[String, MangoPayApi] = Map.empty + + lazy val softPayProvider: SoftPayAccount.Client.Provider = + SoftPayAccount.Client.Provider.defaultInstance + .withProviderType(SoftPayAccount.Client.Provider.ProviderType.MANGOPAY) + .withProviderId(MangoPaySettings.MangoPayConfig.clientId) + .withProviderApiKey(MangoPaySettings.MangoPayConfig.apiKey) - def apply(): MangoPayApi = { - maybeMangoPayApi match { + def apply(provider: SoftPayAccount.Client.Provider): MangoPayApi = { + mangoPayApis.get(provider.providerId) match { case Some(mangoPayApi) => mangoPayApi case _ => // init MangoPay api import MangoPaySettings.MangoPayConfig._ val mangoPayApi = new MangoPayApi mangoPayApi.getConfig.setBaseUrl(baseUrl) - mangoPayApi.getConfig.setClientId(clientId) - mangoPayApi.getConfig.setClientPassword(apiKey) + mangoPayApi.getConfig.setClientId(provider.providerId) + mangoPayApi.getConfig.setClientPassword(provider.providerApiKey) mangoPayApi.getConfig.setDebugMode(debug) // init MangoPay hooks import scala.collection.JavaConverters._ @@ -78,7 +84,7 @@ object MangoPay extends StrictLogging { createOrUpdateHook(mangoPayApi, EventType.MANDATE_CREATED, hooks) createOrUpdateHook(mangoPayApi, EventType.MANDATE_ACTIVATED, hooks) createOrUpdateHook(mangoPayApi, EventType.MANDATE_EXPIRED, hooks) - maybeMangoPayApi = Some(mangoPayApi) + mangoPayApis = mangoPayApis.updated(provider.providerId, mangoPayApi) mangoPayApi } } diff --git a/mangopay/src/main/scala/app/softnetwork/payment/handlers/MangoPayPaymentDao.scala b/mangopay/src/main/scala/app/softnetwork/payment/handlers/MangoPayPaymentDao.scala deleted file mode 100644 index 20f104e..0000000 --- a/mangopay/src/main/scala/app/softnetwork/payment/handlers/MangoPayPaymentDao.scala +++ /dev/null @@ -1,7 +0,0 @@ -package app.softnetwork.payment.handlers - -import org.slf4j.{Logger, LoggerFactory} - -object MangoPayPaymentDao extends GenericPaymentDao with MangoPayPaymentHandler { - lazy val log: Logger = LoggerFactory getLogger getClass.getName -} diff --git a/mangopay/src/main/scala/app/softnetwork/payment/handlers/MangoPayPaymentHandler.scala b/mangopay/src/main/scala/app/softnetwork/payment/handlers/MangoPayPaymentHandler.scala deleted file mode 100644 index 7db51f0..0000000 --- a/mangopay/src/main/scala/app/softnetwork/payment/handlers/MangoPayPaymentHandler.scala +++ /dev/null @@ -1,9 +0,0 @@ -package app.softnetwork.payment.handlers - -import org.slf4j.{Logger, LoggerFactory} - -object MangoPayPaymentHandler extends MangoPayPaymentHandler { - lazy val log: Logger = LoggerFactory getLogger getClass.getName -} - -trait MangoPayPaymentHandler extends GenericPaymentHandler with MangoPayPaymentTypeKey diff --git a/mangopay/src/main/scala/app/softnetwork/payment/handlers/MangoPayPaymentTypeKey.scala b/mangopay/src/main/scala/app/softnetwork/payment/handlers/MangoPayPaymentTypeKey.scala deleted file mode 100644 index 51fdf47..0000000 --- a/mangopay/src/main/scala/app/softnetwork/payment/handlers/MangoPayPaymentTypeKey.scala +++ /dev/null @@ -1,15 +0,0 @@ -package app.softnetwork.payment.handlers - -import akka.cluster.sharding.typed.scaladsl.EntityTypeKey -import app.softnetwork.payment.message.PaymentMessages.PaymentCommand -import app.softnetwork.payment.persistence.typed.MangoPayPaymentBehavior -import app.softnetwork.persistence.typed.CommandTypeKey - -import scala.reflect.ClassTag - -/** Created by smanciot on 22/04/2022. - */ -trait MangoPayPaymentTypeKey extends CommandTypeKey[PaymentCommand] { - override def TypeKey(implicit tTag: ClassTag[PaymentCommand]): EntityTypeKey[PaymentCommand] = - MangoPayPaymentBehavior.TypeKey -} diff --git a/mangopay/src/main/scala/app/softnetwork/payment/persistence/query/MangoPayPaymentCommandProcessorStream.scala b/mangopay/src/main/scala/app/softnetwork/payment/persistence/query/MangoPayPaymentCommandProcessorStream.scala deleted file mode 100644 index 06cc8e9..0000000 --- a/mangopay/src/main/scala/app/softnetwork/payment/persistence/query/MangoPayPaymentCommandProcessorStream.scala +++ /dev/null @@ -1,10 +0,0 @@ -package app.softnetwork.payment.persistence.query - -import app.softnetwork.payment.handlers.MangoPayPaymentHandler -import app.softnetwork.persistence.query.{JournalProvider, OffsetProvider} - -trait MangoPayPaymentCommandProcessorStream - extends GenericPaymentCommandProcessorStream - with MangoPayPaymentHandler { - _: JournalProvider with OffsetProvider => -} diff --git a/mangopay/src/main/scala/app/softnetwork/payment/persistence/typed/MangoPayPaymentBehavior.scala b/mangopay/src/main/scala/app/softnetwork/payment/persistence/typed/MangoPayPaymentBehavior.scala deleted file mode 100644 index 9f5422b..0000000 --- a/mangopay/src/main/scala/app/softnetwork/payment/persistence/typed/MangoPayPaymentBehavior.scala +++ /dev/null @@ -1,10 +0,0 @@ -package app.softnetwork.payment.persistence.typed - -import app.softnetwork.payment.handlers.{GenericPaymentDao, MangoPayPaymentDao} -import app.softnetwork.payment.spi.MangoPayProvider - -case object MangoPayPaymentBehavior extends MangoPayPaymentBehavior { - override lazy val paymentDao: GenericPaymentDao = MangoPayPaymentDao -} - -trait MangoPayPaymentBehavior extends GenericPaymentBehavior with MangoPayProvider diff --git a/mangopay/src/main/scala/app/softnetwork/payment/service/MangoPayPaymentService.scala b/mangopay/src/main/scala/app/softnetwork/payment/service/MangoPayPaymentService.scala index 4a5382a..cb6a615 100644 --- a/mangopay/src/main/scala/app/softnetwork/payment/service/MangoPayPaymentService.scala +++ b/mangopay/src/main/scala/app/softnetwork/payment/service/MangoPayPaymentService.scala @@ -1,10 +1,9 @@ package app.softnetwork.payment.service -import akka.actor.typed.ActorSystem import akka.http.scaladsl.model.{HttpResponse, StatusCodes} import akka.http.scaladsl.server.Route import app.softnetwork.payment.config.PaymentSettings.HooksRoute -import app.softnetwork.payment.handlers.MangoPayPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages.{ InvalidateRegularUser, KycDocumentStatusUpdated, @@ -19,11 +18,14 @@ import app.softnetwork.payment.message.PaymentMessages.{ ValidateRegularUser } import app.softnetwork.payment.model.{BankAccount, KycDocument, UboDeclaration} -import app.softnetwork.session.service.SessionService +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials import com.mangopay.core.enumerations.EventType -import org.slf4j.{Logger, LoggerFactory} -trait MangoPayPaymentService extends GenericPaymentService with MangoPayPaymentHandler { +trait MangoPayPaymentService[SD <: SessionData with SessionDataDecorator[SD]] + extends PaymentService[SD] + with PaymentHandler { + _: SessionMaterials[SD] => def completeWithKycDocumentUpdatedResult( eventType: String, @@ -221,13 +223,3 @@ trait MangoPayPaymentService extends GenericPaymentService with MangoPayPaymentH } } } - -object MangoPayPaymentService { - def apply(_system: ActorSystem[_], _sessionService: SessionService): MangoPayPaymentService = { - new MangoPayPaymentService { - lazy val log: Logger = LoggerFactory getLogger getClass.getName - override implicit def system: ActorSystem[_] = _system - override def sessionService: SessionService = _sessionService - } - } -} diff --git a/mangopay/src/main/scala/app/softnetwork/payment/service/MangoPayPaymentEndpoints.scala b/mangopay/src/main/scala/app/softnetwork/payment/service/MangoPayPaymentServiceEndpoints.scala similarity index 93% rename from mangopay/src/main/scala/app/softnetwork/payment/service/MangoPayPaymentEndpoints.scala rename to mangopay/src/main/scala/app/softnetwork/payment/service/MangoPayPaymentServiceEndpoints.scala index 60930a9..6707598 100644 --- a/mangopay/src/main/scala/app/softnetwork/payment/service/MangoPayPaymentEndpoints.scala +++ b/mangopay/src/main/scala/app/softnetwork/payment/service/MangoPayPaymentServiceEndpoints.scala @@ -1,18 +1,20 @@ package app.softnetwork.payment.service -import akka.actor.typed.ActorSystem import app.softnetwork.payment.config.PaymentSettings -import app.softnetwork.payment.handlers.MangoPayPaymentHandler +import app.softnetwork.payment.handlers.PaymentHandler import app.softnetwork.payment.message.PaymentMessages._ import app.softnetwork.payment.model.{BankAccount, KycDocument, UboDeclaration} -import app.softnetwork.session.service.SessionEndpoints +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials import com.mangopay.core.enumerations.EventType -import org.slf4j.{Logger, LoggerFactory} import sttp.tapir.server.ServerEndpoint.Full import scala.concurrent.Future -trait MangoPayPaymentEndpoints extends GenericPaymentEndpoints with MangoPayPaymentHandler { +trait MangoPayPaymentServiceEndpoints[SD <: SessionData with SessionDataDecorator[SD]] + extends PaymentServiceEndpoints[SD] + with PaymentHandler { + _: SessionMaterials[SD] => /** should be implemented by each payment provider */ @@ -252,16 +254,3 @@ trait MangoPayPaymentEndpoints extends GenericPaymentEndpoints with MangoPayPaym } } - -object MangoPayPaymentEndpoints { - def apply( - _system: ActorSystem[_], - _sessionEndpoints: SessionEndpoints - ): MangoPayPaymentEndpoints = { - new MangoPayPaymentEndpoints { - lazy val log: Logger = LoggerFactory getLogger getClass.getName - override implicit def system: ActorSystem[_] = _system - override def sessionEndpoints: SessionEndpoints = _sessionEndpoints - } - } -} 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 273e22d..29be0a6 100644 --- a/mangopay/src/main/scala/app/softnetwork/payment/spi/MangoPayProvider.scala +++ b/mangopay/src/main/scala/app/softnetwork/payment/spi/MangoPayProvider.scala @@ -23,7 +23,8 @@ import com.mangopay.entities.subentities.{BrowserInfo => MangoPayBrowserInfo, _} import app.softnetwork.payment.model.{RecurringPayment, _} import app.softnetwork.payment.config.{MangoPay, MangoPaySettings} import app.softnetwork.payment.config.MangoPaySettings.MangoPayConfig._ -import app.softnetwork.payment.model.PaymentUser.PaymentUserType +import app.softnetwork.payment.model.NaturalUser.NaturalUserType +import app.softnetwork.payment.model.SoftPayAccount.Client.Provider import scala.util.{Failure, Success, Try} import app.softnetwork.persistence._ @@ -204,7 +205,7 @@ trait MangoPayProvider extends PaymentProvider { * @return * provider user id */ - def createOrUpdateNaturalUser(maybeNaturalUser: Option[PaymentUser]): Option[String] = { + def createOrUpdateNaturalUser(maybeNaturalUser: Option[NaturalUser]): Option[String] = { maybeNaturalUser match { case Some(naturalUser) => import naturalUser._ @@ -221,10 +222,10 @@ trait MangoPayProvider extends PaymentProvider { user.setTag(externalUuid) user.setNationality(CountryIso.valueOf(nationality)) user.setCountryOfResidence(CountryIso.valueOf(countryOfResidence)) - paymentUserType match { + naturalUserType match { case Some(value) => value match { - case PaymentUserType.PAYER => user.setUserCategory(UserCategory.PAYER) + case NaturalUserType.PAYER => user.setUserCategory(UserCategory.PAYER) case _ => user.setUserCategory(UserCategory.OWNER) user.setTermsAndConditionsAccepted(true) @@ -234,7 +235,7 @@ trait MangoPayProvider extends PaymentProvider { (if (userId.getOrElse("").trim.isEmpty) None else - Try(MangoPay().getUserApi.getNatural(userId.getOrElse(""))) match { + Try(MangoPay(provider).getUserApi.getNatural(userId.getOrElse(""))) match { case Success(u) => Option(u) case Failure(f) => mlog.error(f.getMessage, f) @@ -242,14 +243,14 @@ trait MangoPayProvider extends PaymentProvider { }) match { case Some(u) => user.setId(u.getId) - Try(MangoPay().getUserApi.update(user).getId) match { + Try(MangoPay(provider).getUserApi.update(user).getId) match { case Success(id) => Some(id) case Failure(f) => mlog.error(f.getMessage, f) None } case None => - Try(MangoPay().getUserApi.create(user).getId) match { + Try(MangoPay(provider).getUserApi.create(user).getId) match { case Success(id) => Some(id) case Failure(f) => mlog.error(f.getMessage, f) @@ -306,10 +307,10 @@ trait MangoPayProvider extends PaymentProvider { ) user.setEmail(legalRepresentative.email) user.setCompanyNumber(siret) - legalRepresentative.paymentUserType match { + legalRepresentative.naturalUserType match { case Some(value) => value match { - case PaymentUserType.PAYER => user.setUserCategory(UserCategory.PAYER) + case NaturalUserType.PAYER => user.setUserCategory(UserCategory.PAYER) case _ => user.setUserCategory(UserCategory.OWNER) user.setTermsAndConditionsAccepted(true) @@ -322,7 +323,9 @@ trait MangoPayProvider extends PaymentProvider { (if (legalRepresentative.userId.isEmpty) None else - Try(MangoPay().getUserApi.getLegal(legalRepresentative.userId.getOrElse(""))) match { + Try( + MangoPay(provider).getUserApi.getLegal(legalRepresentative.userId.getOrElse("")) + ) match { case Success(u) => Option(u) case Failure(f) => mlog.error(f.getMessage, f) @@ -330,14 +333,14 @@ trait MangoPayProvider extends PaymentProvider { }) match { case Some(u) => user.setId(u.getId) - Try(MangoPay().getUserApi.update(user).getId) match { + Try(MangoPay(provider).getUserApi.update(user).getId) match { case Success(id) => Some(id) case Failure(f) => mlog.error(f.getMessage, f) None } case None => - Try(MangoPay().getUserApi.create(user).getId) match { + Try(MangoPay(provider).getUserApi.create(user).getId) match { case Success(id) => Some(id) case Failure(f) => mlog.error(f.getMessage, f) @@ -377,7 +380,7 @@ trait MangoPayProvider extends PaymentProvider { wallet.setDescription(s"wallet for $externalUuid") wallet.setTag(externalUuid) Try( - MangoPay().getUserApi + MangoPay(provider).getUserApi .getWallets(userId) .asScala .find(w => @@ -388,14 +391,14 @@ trait MangoPayProvider extends PaymentProvider { maybeWallet match { case Some(w) => wallet.setId(w.getId) - Try(MangoPay().getWalletApi.update(wallet).getId) match { + Try(MangoPay(provider).getWalletApi.update(wallet).getId) match { case Success(id) => Some(id) case Failure(f) => mlog.error(f.getMessage, f) None } case None => - Try(MangoPay().getWalletApi.create(wallet).getId) match { + Try(MangoPay(provider).getWalletApi.create(wallet).getId) match { case Success(id) => Some(id) case Failure(f) => mlog.error(f.getMessage, f) @@ -441,7 +444,7 @@ trait MangoPayProvider extends PaymentProvider { bankAccount.setType(BankAccountType.IBAN) bankAccount.setUserId(userId) (if (id.isDefined) - Try(MangoPay().getUserApi.getBankAccount(userId, getId)) match { + Try(MangoPay(provider).getUserApi.getBankAccount(userId, getId)) match { case Success(b) => Option(b) case Failure(f) => mlog.error(f.getMessage, f) @@ -450,7 +453,7 @@ trait MangoPayProvider extends PaymentProvider { else None) match { case Some(previousBankAccount) => Try( - MangoPay().getUserApi + MangoPay(provider).getUserApi .updateBankAccount(userId, bankAccount, previousBankAccount.getId) .getId ) match { @@ -460,7 +463,7 @@ trait MangoPayProvider extends PaymentProvider { None } case _ => - Try(MangoPay().getUserApi.createBankAccount(userId, bankAccount).getId) match { + Try(MangoPay(provider).getUserApi.createBankAccount(userId, bankAccount).getId) match { case Success(id) => Option(id) case Failure(f) => mlog.error(f.getMessage, f) @@ -478,7 +481,7 @@ trait MangoPayProvider extends PaymentProvider { */ override def getActiveBankAccount(userId: String): Option[String] = { Try( - MangoPay().getUserApi + MangoPay(provider).getUserApi .getBankAccounts(userId) .asScala .filter(bankAccount => bankAccount.isActive) @@ -504,7 +507,7 @@ trait MangoPayProvider extends PaymentProvider { */ def checkBankAccount(userId: String, bankAccountId: String): Boolean = { Try( - MangoPay().getUserApi + MangoPay(provider).getUserApi .getBankAccounts(userId) .asScala .find(bankAccount => bankAccount.getId == bankAccountId) @@ -538,7 +541,7 @@ trait MangoPayProvider extends PaymentProvider { cardPreRegistration.setCurrency(CurrencyIso.valueOf(currency)) cardPreRegistration.setTag(externalUuid) cardPreRegistration.setUserId(userId) - Try(MangoPay().getCardRegistrationApi.create(cardPreRegistration)) match { + Try(MangoPay(provider).getCardRegistrationApi.create(cardPreRegistration)) match { case Success(cardRegistration) => Some( CardPreRegistration.defaultInstance @@ -569,10 +572,12 @@ trait MangoPayProvider extends PaymentProvider { ): Option[String] = { maybeRegistrationData match { case Some(registrationData) => - Try(MangoPay().getCardRegistrationApi.get(cardPreRegistrationId)) match { + Try(MangoPay(provider).getCardRegistrationApi.get(cardPreRegistrationId)) match { case Success(cardRegistration) => cardRegistration.setRegistrationData(registrationData) - Try(MangoPay().getCardRegistrationApi.update(cardRegistration).getCardId) match { + Try( + MangoPay(provider).getCardRegistrationApi.update(cardRegistration).getCardId + ) match { case Success(cardId) => Option(cardId) case Failure(f) => @@ -594,7 +599,7 @@ trait MangoPayProvider extends PaymentProvider { * card */ def loadCard(cardId: String): Option[Card] = { - Try(MangoPay().getCardApi.get(cardId)) match { + Try(MangoPay(provider).getCardApi.get(cardId)) match { case Success(card) => Some( Card.defaultInstance @@ -615,9 +620,9 @@ trait MangoPayProvider extends PaymentProvider { * the card disabled or none */ override def disableCard(cardId: String): Option[Card] = { - Try(MangoPay().getCardApi.get(cardId)) match { + Try(MangoPay(provider).getCardApi.get(cardId)) match { case Success(card) => - Try(MangoPay().getCardApi.disable(card)) match { + Try(MangoPay(provider).getCardApi.disable(card)) match { case Success(disabledCard) => Some( Card.defaultInstance @@ -686,8 +691,9 @@ trait MangoPayProvider extends PaymentProvider { } Try( idempotencyKey match { - case Some(s) => MangoPay().getCardPreAuthorizationApi.create(s, cardPreAuthorization) - case _ => MangoPay().getCardPreAuthorizationApi.create(cardPreAuthorization) + case Some(s) => + MangoPay(provider).getCardPreAuthorizationApi.create(s, cardPreAuthorization) + case _ => MangoPay(provider).getCardPreAuthorizationApi.create(cardPreAuthorization) } ) match { case Success(result) => @@ -761,7 +767,7 @@ trait MangoPayProvider extends PaymentProvider { cardPreAuthorizedTransactionId: String ): Option[Transaction] = { Try( - MangoPay().getCardPreAuthorizationApi.get(cardPreAuthorizedTransactionId) + MangoPay(provider).getCardPreAuthorizationApi.get(cardPreAuthorizedTransactionId) ) match { case Success(result) => Some( @@ -832,8 +838,8 @@ trait MangoPayProvider extends PaymentProvider { } Try( idempotencyKey match { - case Some(s) => MangoPay().getPayInApi.create(s, payIn) - case _ => MangoPay().getPayInApi.create(payIn) + case Some(s) => MangoPay(provider).getPayInApi.create(s, payIn) + case _ => MangoPay(provider).getPayInApi.create(payIn) } ) match { case Success(result) => @@ -911,12 +917,12 @@ trait MangoPayProvider extends PaymentProvider { cardPreAuthorizedTransactionId: String ): Boolean = { Try( - MangoPay().getCardPreAuthorizationApi.get(cardPreAuthorizedTransactionId) + MangoPay(provider).getCardPreAuthorizationApi.get(cardPreAuthorizedTransactionId) ) match { case Success(result) => result.setPaymentStatus(PaymentStatus.CANCELED) Try( - MangoPay().getCardPreAuthorizationApi.update(result) + MangoPay(provider).getCardPreAuthorizationApi.update(result) ) match { case Success(_) => true case Failure(f) => @@ -941,12 +947,12 @@ trait MangoPayProvider extends PaymentProvider { cardPreAuthorizedTransactionId: String ): Boolean = { Try( - MangoPay().getCardPreAuthorizationApi.get(cardPreAuthorizedTransactionId) + MangoPay(provider).getCardPreAuthorizationApi.get(cardPreAuthorizedTransactionId) ) match { case Success(result) => result.setPaymentStatus(PaymentStatus.VALIDATED) Try( - MangoPay().getCardPreAuthorizationApi.update(result) + MangoPay(provider).getCardPreAuthorizationApi.update(result) ) match { case Success(_) => true case Failure(f) => @@ -1024,8 +1030,8 @@ trait MangoPayProvider extends PaymentProvider { } Try( idempotencyKey match { - case Some(s) => MangoPay().getPayInApi.create(s, payIn) - case _ => MangoPay().getPayInApi.create(payIn) + case Some(s) => MangoPay(provider).getPayInApi.create(s, payIn) + case _ => MangoPay(provider).getPayInApi.create(payIn) } ) match { case Success(result) => @@ -1124,7 +1130,7 @@ trait MangoPayProvider extends PaymentProvider { payIn.setExecutionDetails(executionDetails) payIn.setExecutionType(PayInExecutionType.WEB) Try( - MangoPay().getPayInApi.create(payIn) + MangoPay(provider).getPayInApi.create(payIn) ) match { case Success(result) => Some( @@ -1205,8 +1211,9 @@ trait MangoPayProvider extends PaymentProvider { } Try( idempotencyKey match { - case Some(s) => MangoPay().getPayInApi.createRefund(s, payInTransactionId, refund) - case _ => MangoPay().getPayInApi.createRefund(payInTransactionId, refund) + case Some(s) => + MangoPay(provider).getPayInApi.createRefund(s, payInTransactionId, refund) + case _ => MangoPay(provider).getPayInApi.createRefund(payInTransactionId, refund) } ) match { case Success(result) => @@ -1289,7 +1296,7 @@ trait MangoPayProvider extends PaymentProvider { transfer.getFees.setAmount(feesAmount) transfer.getFees.setCurrency(CurrencyIso.valueOf(currency)) transfer.setDebitedWalletId(debitedWalletId) - Try(MangoPay().getTransferApi.create(transfer)) match { + Try(MangoPay(provider).getTransferApi.create(transfer)) match { case Success(result) => Some( Transaction() @@ -1388,8 +1395,8 @@ trait MangoPayProvider extends PaymentProvider { } Try( idempotencyKey match { - case Some(s) => MangoPay().getPayOutApi.create(s, payOut) - case _ => MangoPay().getPayOutApi.create(payOut) + case Some(s) => MangoPay(provider).getPayOutApi.create(s, payOut) + case _ => MangoPay(provider).getPayOutApi.create(payOut) } ) match { case Success(result) => @@ -1468,7 +1475,7 @@ trait MangoPayProvider extends PaymentProvider { transactionId: String, recurringPayInRegistrationId: Option[String] ): Option[Transaction] = { - Try(MangoPay().getPayInApi.get(transactionId)) match { + Try(MangoPay(provider).getPayInApi.get(transactionId)) match { case Success(result) => val `type` = if (result.getPaymentType == PayInPaymentType.DIRECT_DEBIT) { @@ -1571,7 +1578,7 @@ trait MangoPayProvider extends PaymentProvider { * Refund transaction */ override def loadRefund(orderUuid: String, transactionId: String): Option[Transaction] = { - Try(MangoPay().getPayInApi.getRefund(transactionId)) match { + Try(MangoPay(provider).getPayInApi.getRefund(transactionId)) match { case Success(result) => Some( Transaction().copy( @@ -1603,7 +1610,7 @@ trait MangoPayProvider extends PaymentProvider { * pay out transaction */ override def loadPayOut(orderUuid: String, transactionId: String): Option[Transaction] = { - Try(MangoPay().getPayOutApi.get(transactionId)) match { + Try(MangoPay(provider).getPayOutApi.get(transactionId)) match { case Success(result) => Some( Transaction().copy( @@ -1632,7 +1639,7 @@ trait MangoPayProvider extends PaymentProvider { * transfer transaction */ override def loadTransfer(transactionId: String): Option[Transaction] = { - Try(MangoPay().getTransferApi.get(transactionId)) match { + Try(MangoPay(provider).getTransferApi.get(transactionId)) match { case Success(result) => Some( Transaction().copy( @@ -1694,19 +1701,19 @@ trait MangoPayProvider extends PaymentProvider { documentType: KycDocument.KycDocumentType ): Option[String] = { // create document - Try(MangoPay().getUserApi.createKycDocument(userId, documentType, externalUuid)) match { + Try(MangoPay(provider).getUserApi.createKycDocument(userId, documentType, externalUuid)) match { case Success(s) => mlog.info(s"""Create $documentType for $externalUuid""") // ask for document creation s.setTag(externalUuid) s.setStatus(KycStatus.CREATED) - Try(MangoPay().getUserApi.updateKycDocument(userId, s)) match { + Try(MangoPay(provider).getUserApi.updateKycDocument(userId, s)) match { case Success(s2) => mlog.info(s"""Update $documentType for $externalUuid""") // add document pages if ( pages.forall { page => - Try(MangoPay().getUserApi.createKycPage(userId, s2.getId, page)) match { + Try(MangoPay(provider).getUserApi.createKycPage(userId, s2.getId, page)) match { case Success(_) => mlog.info(s"""Add document page for $externalUuid""") true @@ -1719,7 +1726,7 @@ trait MangoPayProvider extends PaymentProvider { // ask for document validation s2.setTag(externalUuid) s2.setStatus(KycStatus.VALIDATION_ASKED) - Try(MangoPay().getUserApi.updateKycDocument(userId, s2)) match { + Try(MangoPay(provider).getUserApi.updateKycDocument(userId, s2)) match { case Success(s3) => mlog.info(s"""Ask document ${s3.getId} validation for $externalUuid""") Some(s3.getId) @@ -1749,7 +1756,7 @@ trait MangoPayProvider extends PaymentProvider { */ override def loadDocumentStatus(userId: String, documentId: String): KycDocumentValidationReport = // load document - Try(MangoPay().getUserApi.getKycDocument(userId, documentId)) match { + Try(MangoPay(provider).getUserApi.getKycDocument(userId, documentId)) match { case Success(s) => KycDocumentValidationReport.defaultInstance .withId(documentId) @@ -1796,8 +1803,8 @@ trait MangoPayProvider extends PaymentProvider { mandate.setUserId(userId) Try( idempotencyKey match { - case Some(key) => MangoPay().getMandateApi.create(key, mandate) - case _ => MangoPay().getMandateApi.create(mandate) + case Some(key) => MangoPay(provider).getMandateApi.create(key, mandate) + case _ => MangoPay(provider).getMandateApi.create(mandate) } ) match { case Success(s) => @@ -1838,11 +1845,11 @@ trait MangoPayProvider extends PaymentProvider { ): Option[MandateResult] = { Try( maybeMandateId match { - case Some(mandateId) => Option(MangoPay().getMandateApi.get(mandateId)) + case Some(mandateId) => Option(MangoPay(provider).getMandateApi.get(mandateId)) case None => val sorting = new Sorting() sorting.addField("creationDate", SortDirection.desc) - MangoPay().getMandateApi + MangoPay(provider).getMandateApi .getForBankAccount( userId, bankAccountId, @@ -1885,7 +1892,7 @@ trait MangoPayProvider extends PaymentProvider { * mandate result */ override def cancelMandate(mandateId: String): Option[MandateResult] = { - Try(MangoPay().getMandateApi.cancel(mandateId)) match { + Try(MangoPay(provider).getMandateApi.cancel(mandateId)) match { case Success(mandate) => Some( MandateResult.defaultInstance @@ -1960,8 +1967,8 @@ trait MangoPayProvider extends PaymentProvider { } Try( idempotencyKey match { - case Some(s) => MangoPay().getPayInApi.create(s, payIn) - case _ => MangoPay().getPayInApi.create(payIn) + case Some(s) => MangoPay(provider).getPayInApi.create(s, payIn) + case _ => MangoPay(provider).getPayInApi.create(payIn) } ) match { case Success(result) => @@ -2030,7 +2037,7 @@ trait MangoPayProvider extends PaymentProvider { filters.setType(MangoPayTransactionType.PAYIN) filters.setAfterDate(transactionDate.toEpochSecond) Try( - MangoPay().getWalletApi + MangoPay(provider).getWalletApi .getTransactions( walletId, new Pagination(1, 100), @@ -2075,11 +2082,44 @@ trait MangoPayProvider extends PaymentProvider { } } + override def client: Option[SoftPayAccount.Client] = { + Try(MangoPay(provider).getClientApi.get()) match { + case Success(client) => + Some( + SoftPayAccount.Client.defaultInstance + .withClientId(client.getClientId + "." + provider.providerType.name.toLowerCase) + .withProvider(provider) + .copy( + name = Option(client.getName), + description = Option(client.getPlatformDescription), + logoUrl = Option(client.getLogo), + websiteUrl = Option(client.getPlatformUrl), + address = Option(client.getHeadquartersAddress).map(address => + Address.defaultInstance + .withAddressLine(address.getAddressLine1) + .withCity(address.getCity) + .withPostalCode(address.getPostalCode) + .withCountry(address.getCountry.name()) + .copy(state = Option(address.getRegion)) + ), + technicalEmails = client.getTechEmails.asScala, + administrativeEmails = client.getAdminEmails.asScala, + billingEmails = client.getBillingEmails.asScala, + fraudEmails = client.getFraudEmails.asScala, + vatNumber = Option(client.getTaxNumber) + ) + ) + case Failure(f) => + mlog.error(f.getMessage, f) + None + } + } + /** @return * client fees */ override def clientFees(): Option[Double] = { - Try(MangoPay().getClientApi.getWallet(FundsType.FEES, CurrencyIso.EUR)) match { + Try(MangoPay(provider).getClientApi.getWallet(FundsType.FEES, CurrencyIso.EUR)) match { case Success(wallet) => Some(wallet.getBalance.getAmount.toDouble / 100) case Failure(f) => mlog.error(f.getMessage, f) @@ -2093,7 +2133,7 @@ trait MangoPayProvider extends PaymentProvider { * Ultimate Beneficial Owner Declaration */ override def createDeclaration(userId: String): Option[UboDeclaration] = { - Try(MangoPay().getUboDeclarationApi.create(userId)) match { + Try(MangoPay(provider).getUboDeclarationApi.create(userId)) match { case Success(declaration) => Some( UboDeclaration.defaultInstance @@ -2152,9 +2192,9 @@ trait MangoPayProvider extends PaymentProvider { { if (id.isEmpty) { - Try(MangoPay().getUboDeclarationApi.createUbo(userId, uboDeclarationId, ubo)) + Try(MangoPay(provider).getUboDeclarationApi.createUbo(userId, uboDeclarationId, ubo)) } else { - Try(MangoPay().getUboDeclarationApi.updateUbo(userId, uboDeclarationId, ubo)) + Try(MangoPay(provider).getUboDeclarationApi.updateUbo(userId, uboDeclarationId, ubo)) } } match { case Success(s2) => @@ -2177,7 +2217,7 @@ trait MangoPayProvider extends PaymentProvider { * declaration with Ultimate Beneficial Owner(s) */ override def getDeclaration(userId: String, uboDeclarationId: String): Option[UboDeclaration] = { - Try(MangoPay().getUboDeclarationApi.get(userId, uboDeclarationId)) match { + Try(MangoPay(provider).getUboDeclarationApi.get(userId, uboDeclarationId)) match { case Success(s) => import scala.collection.JavaConverters._ Some( @@ -2226,7 +2266,9 @@ trait MangoPayProvider extends PaymentProvider { userId: String, uboDeclarationId: String ): Option[UboDeclaration] = { - Try(MangoPay().getUboDeclarationApi.submitForValidation(userId, uboDeclarationId)) match { + Try( + MangoPay(provider).getUboDeclarationApi.submitForValidation(userId, uboDeclarationId) + ) match { case Success(s) => Some( UboDeclaration.defaultInstance @@ -2323,7 +2365,8 @@ trait MangoPayProvider extends PaymentProvider { case _ => } Try( - MangoPay().getPayInApi.createRecurringPayment(generateUUID(), createRecurringPayment) + MangoPay(provider).getPayInApi + .createRecurringPayment(generateUUID(), createRecurringPayment) ) match { case Success(s) => Some( @@ -2366,7 +2409,7 @@ trait MangoPayProvider extends PaymentProvider { case _ => } Try( - MangoPay().getPayInApi + MangoPay(provider).getPayInApi .updateRecurringPayment(recurringPayInRegistrationId, recurringPaymentUpdate) ) match { case Success(s) => @@ -2394,7 +2437,7 @@ trait MangoPayProvider extends PaymentProvider { recurringPayInRegistrationId: String ): Option[RecurringPayment.RecurringCardPaymentResult] = { Try( - MangoPay().getPayInApi.getRecurringPayment(recurringPayInRegistrationId) + MangoPay(provider).getPayInApi.getRecurringPayment(recurringPayInRegistrationId) ) match { case Success(s) => Some( @@ -2458,7 +2501,7 @@ trait MangoPayProvider extends PaymentProvider { s"$recurringPaymentFor3DS/$recurringPayInRegistrationId" ) Try( - MangoPay().getPayInApi.createRecurringPayInCIT(generateUUID(), recurringPayInCIT) + MangoPay(provider).getPayInApi.createRecurringPayInCIT(generateUUID(), recurringPayInCIT) ) match { case Success(result) => Some( @@ -2511,7 +2554,7 @@ trait MangoPayProvider extends PaymentProvider { recurringPayInMIT.setFees(fees) recurringPayInMIT.setTag(externalUuid) Try( - MangoPay().getPayInApi.createRecurringPayInMIT(generateUUID(), recurringPayInMIT) + MangoPay(provider).getPayInApi.createRecurringPayInMIT(generateUUID(), recurringPayInMIT) ) match { case Success(result) => Some( @@ -2548,3 +2591,16 @@ trait MangoPayProvider extends PaymentProvider { } } + +class MangoPayProviderFactory extends PaymentProviderSpi { + override val providerType: Provider.ProviderType = + Provider.ProviderType.MANGOPAY + + override def paymentProvider(p: SoftPayAccount.Client.Provider): MangoPayProvider = + new MangoPayProvider { + override implicit val provider: SoftPayAccount.Client.Provider = p + } + + override def softPaymentProvider: SoftPayAccount.Client.Provider = + MangoPay.softPayProvider +} diff --git a/project/Versions.scala b/project/Versions.scala index d229cde..230372d 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,8 +1,18 @@ object Versions { - val genericPersistence = "0.4.1" + val genericPersistence = "0.6.2" - val scheduler = "0.4.1" + val scheduler = "0.6.3" + + val notification = "0.6.3" + + val account = "0.6.2.1" val scalatest = "3.2.16" + + val scopt = "4.0.1" + + val jinja = "2.7.1" + + val scalate = "1.9.8" } diff --git a/testkit/build.sbt b/testkit/build.sbt index c5fa90e..5cdb86d 100644 --- a/testkit/build.sbt +++ b/testkit/build.sbt @@ -9,5 +9,6 @@ libraryDependencies ++= Seq( "app.softnetwork.api" %% "generic-server-api-testkit" % Versions.genericPersistence, "app.softnetwork.session" %% "session-testkit" % Versions.genericPersistence, "app.softnetwork.persistence" %% "persistence-core-testkit" % Versions.genericPersistence, + "app.softnetwork.account" %% "account-testkit" % Versions.account, "org.scalatest" %% "scalatest" % Versions.scalatest ) diff --git a/testkit/src/main/resources/META-INF/services/app.softnetwork.payment.spi.PaymentProviderSpi b/testkit/src/main/resources/META-INF/services/app.softnetwork.payment.spi.PaymentProviderSpi new file mode 100644 index 0000000..e4f69d5 --- /dev/null +++ b/testkit/src/main/resources/META-INF/services/app.softnetwork.payment.spi.PaymentProviderSpi @@ -0,0 +1 @@ +app.softnetwork.payment.spi.MockMangoPayProviderFactory diff --git a/mangopay/src/main/scala/app/softnetwork/payment/api/MangoPayServer.scala b/testkit/src/main/scala/app/softnetwork/payment/api/MockClientServer.scala similarity index 51% rename from mangopay/src/main/scala/app/softnetwork/payment/api/MangoPayServer.scala rename to testkit/src/main/scala/app/softnetwork/payment/api/MockClientServer.scala index 9a08607..7c038f6 100644 --- a/mangopay/src/main/scala/app/softnetwork/payment/api/MangoPayServer.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/api/MockClientServer.scala @@ -1,14 +1,14 @@ package app.softnetwork.payment.api import akka.actor.typed.ActorSystem -import app.softnetwork.payment.handlers.MangoPayPaymentTypeKey +import app.softnetwork.payment.handlers.MockSoftPayAccountDao import org.slf4j.{Logger, LoggerFactory} -trait MangoPayServer extends PaymentServer with MangoPayPaymentTypeKey +trait MockClientServer extends ClientServer with MockSoftPayAccountDao -object MangoPayServer { - def apply(sys: ActorSystem[_]): MangoPayServer = { - new MangoPayServer { +object MockClientServer { + def apply(sys: ActorSystem[_]): MockClientServer = { + new MockClientServer { lazy val log: Logger = LoggerFactory getLogger getClass.getName override implicit val system: ActorSystem[_] = sys } diff --git a/testkit/src/main/scala/app/softnetwork/payment/api/MockPaymentServer.scala b/testkit/src/main/scala/app/softnetwork/payment/api/MockPaymentServer.scala index 77ab6f4..39f4a43 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/api/MockPaymentServer.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/api/MockPaymentServer.scala @@ -1,10 +1,10 @@ package app.softnetwork.payment.api import akka.actor.typed.ActorSystem -import app.softnetwork.payment.handlers.MockPaymentTypeKey +import app.softnetwork.payment.handlers.MockPaymentDao import org.slf4j.{Logger, LoggerFactory} -trait MockPaymentServer extends PaymentServer with MockPaymentTypeKey +trait MockPaymentServer extends PaymentServer with MockPaymentDao object MockPaymentServer { def apply(sys: ActorSystem[_]): MockPaymentServer = { diff --git a/testkit/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServer.scala b/testkit/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServerTestKit.scala similarity index 88% rename from testkit/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServer.scala rename to testkit/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServerTestKit.scala index 1f9d51d..01a3119 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServer.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServerTestKit.scala @@ -6,9 +6,9 @@ import app.softnetwork.persistence.scalatest.InMemoryPersistenceTestKit import app.softnetwork.scheduler.launch.SchedulerGuardian import org.scalatest.Suite -trait PaymentGrpcServer +trait PaymentGrpcServerTestKit extends PersistenceScalatestGrpcTest - with PaymentGrpcServices + with PaymentGrpcServicesTestKit with InMemoryPersistenceTestKit { _: Suite with PaymentGuardian with SchedulerGuardian => override lazy val additionalConfig: String = paymentGrpcConfig } diff --git a/testkit/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServices.scala b/testkit/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServicesTestKit.scala similarity index 55% rename from testkit/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServices.scala rename to testkit/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServicesTestKit.scala index 8b2e363..40735a8 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServices.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/api/PaymentGrpcServicesTestKit.scala @@ -1,35 +1,32 @@ package app.softnetwork.payment.api import akka.actor.typed.ActorSystem -import akka.http.scaladsl.model.{HttpRequest, HttpResponse} +import app.softnetwork.api.server.GrpcService import app.softnetwork.api.server.scalatest.ServerTestKit import app.softnetwork.payment.launch.PaymentGuardian -import app.softnetwork.scheduler.api.SchedulerGrpcServices +import app.softnetwork.scheduler.api.SchedulerGrpcServicesTestKit import app.softnetwork.scheduler.launch.SchedulerGuardian -import scala.concurrent.Future - -trait PaymentGrpcServices extends SchedulerGrpcServices { +trait PaymentGrpcServicesTestKit extends SchedulerGrpcServicesTestKit with SoftPayClientTestKit { _: PaymentGuardian with SchedulerGuardian with ServerTestKit => - override def grpcServices - : ActorSystem[_] => Seq[PartialFunction[HttpRequest, Future[HttpResponse]]] = system => + override def grpcServices: ActorSystem[_] => Seq[GrpcService] = system => paymentGrpcServices(system) ++ schedulerGrpcServices(system) - def paymentGrpcServices - : ActorSystem[_] => Seq[PartialFunction[HttpRequest, Future[HttpResponse]]] = - system => - Seq( - PaymentServiceApiHandler.partial(MockPaymentServer(system))(system) - ) - def paymentGrpcConfig: String = schedulerGrpcConfig + s""" |# Important: enable HTTP/2 in ActorSystem's config |akka.http.server.preview.enable-http2 = on + | + |akka.grpc.client."${Client.name}"{ + | host = $interface + | port = $port + | use-tls = false + |} + | |akka.grpc.client."${PaymentClient.name}"{ | host = $interface | port = $port | use-tls = false |} - |""".stripMargin + |""".stripMargin + softPayClientSettings } diff --git a/testkit/src/main/scala/app/softnetwork/payment/api/SoftPayClientTestKit.scala b/testkit/src/main/scala/app/softnetwork/payment/api/SoftPayClientTestKit.scala new file mode 100644 index 0000000..f2f56f1 --- /dev/null +++ b/testkit/src/main/scala/app/softnetwork/payment/api/SoftPayClientTestKit.scala @@ -0,0 +1,20 @@ +package app.softnetwork.payment.api + +import app.softnetwork.payment.config.MangoPay +import app.softnetwork.payment.model.SoftPayAccount +import app.softnetwork.security.sha256 + +trait SoftPayClientTestKit { + + def provider: SoftPayAccount.Client.Provider = + MangoPay.softPayProvider.withProviderType( + SoftPayAccount.Client.Provider.ProviderType.MOCK + ) + + def softPayClientSettings: String = + s""" + |payment.test = true + |payment.client-id = "${provider.clientId}" + |payment.api-key = "${sha256(provider.providerApiKey)}" + |""".stripMargin +} diff --git a/testkit/src/main/scala/app/softnetwork/payment/data/package.scala b/testkit/src/main/scala/app/softnetwork/payment/data/package.scala index ba13117..a244b32 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/data/package.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/data/package.scala @@ -1,6 +1,6 @@ package app.softnetwork.payment -import app.softnetwork.payment.model.{Address, CardPreRegistration, LegalUser, PaymentUser} +import app.softnetwork.payment.model.{Address, CardPreRegistration, LegalUser, NaturalUser} import app.softnetwork.payment.model.UboDeclaration.UltimateBeneficialOwner import app.softnetwork.payment.model.UboDeclaration.UltimateBeneficialOwner.BirthPlace @@ -25,8 +25,8 @@ package object data { val lastName = "lastName" val birthday = "26/12/1972" val email = "demo@softnetwork.fr" - val naturalUser: PaymentUser = - PaymentUser.defaultInstance + val naturalUser: NaturalUser = + NaturalUser.defaultInstance .withExternalUuid(customerUuid) .withFirstName(firstName) .withLastName(lastName) diff --git a/testkit/src/main/scala/app/softnetwork/payment/handlers/MockPaymentDao.scala b/testkit/src/main/scala/app/softnetwork/payment/handlers/MockPaymentDao.scala index 64bec7a..95bc3d6 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/handlers/MockPaymentDao.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/handlers/MockPaymentDao.scala @@ -2,6 +2,8 @@ package app.softnetwork.payment.handlers import org.slf4j.{Logger, LoggerFactory} -object MockPaymentDao extends GenericPaymentDao with MockPaymentHandler { +trait MockPaymentDao extends PaymentDao with MockPaymentTypeKey + +object MockPaymentDao extends MockPaymentDao { lazy val log: Logger = LoggerFactory getLogger getClass.getName } diff --git a/testkit/src/main/scala/app/softnetwork/payment/handlers/MockPaymentHandler.scala b/testkit/src/main/scala/app/softnetwork/payment/handlers/MockPaymentHandler.scala index 7739f86..b49dd59 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/handlers/MockPaymentHandler.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/handlers/MockPaymentHandler.scala @@ -6,4 +6,4 @@ object MockPaymentHandler extends MockPaymentHandler { lazy val log: Logger = LoggerFactory getLogger getClass.getName } -trait MockPaymentHandler extends GenericPaymentHandler with MockPaymentTypeKey +trait MockPaymentHandler extends PaymentHandler with MockPaymentTypeKey diff --git a/testkit/src/main/scala/app/softnetwork/payment/handlers/MockSoftPayAccountDao.scala b/testkit/src/main/scala/app/softnetwork/payment/handlers/MockSoftPayAccountDao.scala new file mode 100644 index 0000000..096d6d4 --- /dev/null +++ b/testkit/src/main/scala/app/softnetwork/payment/handlers/MockSoftPayAccountDao.scala @@ -0,0 +1,22 @@ +package app.softnetwork.payment.handlers + +import akka.cluster.sharding.typed.scaladsl.EntityTypeKey +import app.softnetwork.account.message.AccountCommand +import app.softnetwork.payment.persistence.typed.MockSoftPayAccountBehavior +import app.softnetwork.persistence.typed.CommandTypeKey +import org.slf4j.{Logger, LoggerFactory} + +import scala.reflect.ClassTag + +trait MockSoftPayAccountTypeKey extends CommandTypeKey[AccountCommand] { + override def TypeKey(implicit tTag: ClassTag[AccountCommand]): EntityTypeKey[AccountCommand] = + MockSoftPayAccountBehavior.TypeKey +} + +trait MockSoftPayAccountHandler extends SoftPayAccountHandler with MockSoftPayAccountTypeKey + +trait MockSoftPayAccountDao extends SoftPayAccountDao with MockSoftPayAccountTypeKey + +object MockSoftPayAccountDao extends MockSoftPayAccountDao { + lazy val log: Logger = LoggerFactory getLogger getClass.getName +} diff --git a/testkit/src/main/scala/app/softnetwork/payment/persistence/query/MockPaymentCommandProcessorStream.scala b/testkit/src/main/scala/app/softnetwork/payment/persistence/query/MockPaymentCommandProcessorStream.scala deleted file mode 100644 index 1a820ca..0000000 --- a/testkit/src/main/scala/app/softnetwork/payment/persistence/query/MockPaymentCommandProcessorStream.scala +++ /dev/null @@ -1,10 +0,0 @@ -package app.softnetwork.payment.persistence.query - -import app.softnetwork.payment.handlers.MockPaymentHandler -import app.softnetwork.persistence.query.{JournalProvider, OffsetProvider} - -trait MockPaymentCommandProcessorStream - extends GenericPaymentCommandProcessorStream - with MockPaymentHandler { - _: JournalProvider with OffsetProvider => -} diff --git a/testkit/src/main/scala/app/softnetwork/payment/persistence/typed/MockPaymentBehavior.scala b/testkit/src/main/scala/app/softnetwork/payment/persistence/typed/MockPaymentBehavior.scala index f7ea2d5..0040960 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/persistence/typed/MockPaymentBehavior.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/persistence/typed/MockPaymentBehavior.scala @@ -1,10 +1,17 @@ package app.softnetwork.payment.persistence.typed -import app.softnetwork.payment.handlers.{GenericPaymentDao, MockPaymentDao} -import app.softnetwork.payment.spi.MockMangoPayProvider +import app.softnetwork.payment.handlers.{ + MockPaymentDao, + MockSoftPayAccountDao, + PaymentDao, + SoftPayAccountDao +} -case object MockPaymentBehavior extends GenericPaymentBehavior with MockMangoPayProvider { +object MockPaymentBehavior extends PaymentBehavior { override def persistenceId = s"Mock${super.persistenceId}" - override lazy val paymentDao: GenericPaymentDao = MockPaymentDao + override lazy val paymentDao: PaymentDao = MockPaymentDao + + override lazy val softPayAccountDao: SoftPayAccountDao = MockSoftPayAccountDao + } diff --git a/testkit/src/main/scala/app/softnetwork/payment/persistence/typed/MockSoftPayAccountBehavior.scala b/testkit/src/main/scala/app/softnetwork/payment/persistence/typed/MockSoftPayAccountBehavior.scala new file mode 100644 index 0000000..83ff837 --- /dev/null +++ b/testkit/src/main/scala/app/softnetwork/payment/persistence/typed/MockSoftPayAccountBehavior.scala @@ -0,0 +1,7 @@ +package app.softnetwork.payment.persistence.typed + +import app.softnetwork.account.handlers.MockGenerator + +object MockSoftPayAccountBehavior extends SoftPayAccountBehavior with MockGenerator { + override def persistenceId = "MockSoftPayAccount" +} diff --git a/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentEndpointsTestKit.scala b/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentEndpointsTestKit.scala index 380838a..8459c4d 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentEndpointsTestKit.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentEndpointsTestKit.scala @@ -1,25 +1,89 @@ package app.softnetwork.payment.scalatest import akka.actor.typed.ActorSystem +import app.softnetwork.account.message.BasicAccountSignUp +import app.softnetwork.account.service.{AccountServiceEndpoints, OAuthServiceEndpoints} import app.softnetwork.api.server.Endpoint -import app.softnetwork.payment.launch.{PaymentEndpoints, PaymentGuardian} -import app.softnetwork.payment.service.{GenericPaymentEndpoints, MockPaymentEndpoints} +import app.softnetwork.payment.handlers.{MockSoftPayAccountDao, SoftPayAccountDao} +import app.softnetwork.payment.launch.PaymentEndpoints +import app.softnetwork.payment.service.{ + MockPaymentServiceEndpoints, + MockSoftPayAccountServiceEndpoints, + MockSoftPayOAuthServiceEndpoints, + PaymentServiceEndpoints +} import app.softnetwork.persistence.schema.SchemaProvider -import app.softnetwork.session.scalatest.SessionEndpointsRoutes +import app.softnetwork.session.scalatest.{SessionEndpointsRoutes, SessionTestKit} import app.softnetwork.session.CsrfCheck +import app.softnetwork.session.model.{SessionData, SessionDataCompanion, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials +import com.softwaremill.session.{RefreshTokenStorage, SessionConfig, SessionManager} +import org.slf4j.{Logger, LoggerFactory} +import org.softnetwork.session.model.{JwtClaims, Session} + +import scala.concurrent.ExecutionContext -trait PaymentEndpointsTestKit extends PaymentEndpoints with SessionEndpointsRoutes { - _: PaymentGuardian with SchemaProvider with CsrfCheck => +trait PaymentEndpointsTestKit[SD <: SessionData with SessionDataDecorator[SD]] + extends PaymentEndpoints[SD] + with SessionEndpointsRoutes[SD] { + self: PaymentTestKit + with SessionTestKit[SD] + with SchemaProvider + with CsrfCheck + with SessionMaterials[SD] => - override def paymentEndpoints: ActorSystem[_] => GenericPaymentEndpoints = system => - MockPaymentEndpoints(system, sessionEndpoints(system)) + implicit def sessionConfig: SessionConfig + + override def paymentEndpoints: ActorSystem[_] => PaymentServiceEndpoints[SD] = sys => + new MockPaymentServiceEndpoints[SD] with SessionMaterials[SD] { + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected def sessionType: Session.SessionType = self.sessionType + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def sessionConfig: SessionConfig = self.sessionConfig + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + override def softPayAccountDao: SoftPayAccountDao = MockSoftPayAccountDao + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage + override implicit def companion: SessionDataCompanion[SD] = self.companion + } override def endpoints: ActorSystem[_] => List[Endpoint] = - system => - List( - sessionServiceEndpoints(system), - paymentEndpoints(system), - MockPaymentEndpoints.swagger(system, sessionEndpoints(system)) - ) + system => super.endpoints(system) :+ sessionServiceEndpoints(system) + + override def accountEndpoints: ActorSystem[_] => AccountServiceEndpoints[BasicAccountSignUp, SD] = + sys => + new MockSoftPayAccountServiceEndpoints[SD] with SessionMaterials[SD] { + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected def sessionType: Session.SessionType = self.sessionType + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + override protected val manifestWrapper: ManifestW = ManifestW() + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage + override implicit def companion: SessionDataCompanion[SD] = self.companion + } + override def oauthEndpoints: ActorSystem[_] => OAuthServiceEndpoints[SD] = sys => + new MockSoftPayOAuthServiceEndpoints[SD] with SessionMaterials[SD] { + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected def sessionType: Session.SessionType = self.sessionType + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + override def softPayAccountDao: SoftPayAccountDao = MockSoftPayAccountDao + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage + override implicit def companion: SessionDataCompanion[SD] = self.companion + } } diff --git a/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentRouteTestKit.scala b/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentRouteTestKit.scala index 5d93d3a..d7acf09 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentRouteTestKit.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentRouteTestKit.scala @@ -1,24 +1,32 @@ package app.softnetwork.payment.scalatest +import akka.actor.typed.ActorSystem import akka.http.scaladsl.model.headers.RawHeader import akka.http.scaladsl.model.{ContentTypes, Multipart, StatusCodes} import app.softnetwork.api.server.ApiRoutes import app.softnetwork.api.server.config.ServerSettings.RootPath -import app.softnetwork.payment.api.PaymentGrpcServices +import app.softnetwork.payment.api.PaymentGrpcServicesTestKit import app.softnetwork.payment.config.PaymentSettings._ import app.softnetwork.payment.model._ +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} import app.softnetwork.session.scalatest.SessionTestKit +import app.softnetwork.session.service.SessionMaterials import org.scalatest.Suite import java.nio.file.Paths -trait PaymentRouteTestKit extends SessionTestKit with PaymentTestKit with PaymentGrpcServices { - _: Suite with ApiRoutes => +trait PaymentRouteTestKit[SD <: SessionData with SessionDataDecorator[SD]] + extends SessionTestKit[SD] + with PaymentTestKit + with PaymentGrpcServicesTestKit { + _: Suite with ApiRoutes with SessionMaterials[SD] => import app.softnetwork.serialization._ override lazy val additionalConfig: String = paymentGrpcConfig + override implicit lazy val ts: ActorSystem[_] = typedSystem() + override def beforeAll(): Unit = { initAndJoinCluster() // pre load routes diff --git a/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentRoutesTestKit.scala b/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentRoutesTestKit.scala index 3f325f4..f23d157 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentRoutesTestKit.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentRoutesTestKit.scala @@ -1,23 +1,94 @@ package app.softnetwork.payment.scalatest import akka.actor.typed.ActorSystem +import app.softnetwork.account.model.{ + DefaultAccountDetailsView, + DefaultAccountView, + DefaultProfileView +} +import app.softnetwork.account.service.{AccountService, OAuthService} import app.softnetwork.api.server.ApiRoute -import app.softnetwork.payment.launch.{PaymentGuardian, PaymentRoutes} -import app.softnetwork.payment.service.{GenericPaymentService, MockPaymentService} +import app.softnetwork.payment.handlers.{MockSoftPayAccountDao, SoftPayAccountDao} +import app.softnetwork.payment.launch.PaymentRoutes +import app.softnetwork.payment.service.{ + MockPaymentService, + MockSoftPayAccountService, + MockSoftPayOAuthService, + PaymentService +} import app.softnetwork.persistence.schema.SchemaProvider -import app.softnetwork.session.scalatest.SessionServiceRoutes +import app.softnetwork.session.model.{SessionData, SessionDataCompanion, SessionDataDecorator} +import app.softnetwork.session.scalatest.{SessionServiceRoutes, SessionTestKit} +import app.softnetwork.session.service.SessionMaterials +import com.softwaremill.session.{RefreshTokenStorage, SessionConfig, SessionManager} +import org.slf4j.{Logger, LoggerFactory} +import org.softnetwork.session.model.Session + +import scala.concurrent.ExecutionContext + +trait PaymentRoutesTestKit[SD <: SessionData with SessionDataDecorator[SD]] + extends PaymentRoutes[SD] + with SessionServiceRoutes[SD] { + self: PaymentTestKit with SessionTestKit[SD] with SchemaProvider with SessionMaterials[SD] => + + override def paymentService: ActorSystem[_] => PaymentService[SD] = sys => + new MockPaymentService[SD] with SessionMaterials[SD] { + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected def sessionType: Session.SessionType = self.sessionType + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def sessionConfig: SessionConfig = self.sessionConfig + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + override def softPayAccountDao: SoftPayAccountDao = MockSoftPayAccountDao + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage + override implicit def companion: SessionDataCompanion[SD] = self.companion + } + + implicit def sessionConfig: SessionConfig -trait PaymentRoutesTestKit extends PaymentRoutes with SessionServiceRoutes { - _: PaymentGuardian with SchemaProvider => + override def accountService: ActorSystem[_] => AccountService[ + DefaultProfileView, + DefaultAccountDetailsView, + DefaultAccountView[DefaultProfileView, DefaultAccountDetailsView], + SD + ] = sys => + new MockSoftPayAccountService[SD] with SessionMaterials[SD] { + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected def sessionType: Session.SessionType = self.sessionType + override def log: Logger = LoggerFactory getLogger getClass.getName + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + override protected val manifestWrapper: ManifestW = ManifestW() + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage + override implicit def companion: SessionDataCompanion[SD] = self.companion + } - override def paymentService: ActorSystem[_] => GenericPaymentService = system => - MockPaymentService(system, sessionService(system)) + override def oauthService: ActorSystem[_] => OAuthService[SD] = sys => + new MockSoftPayOAuthService[SD] with SessionMaterials[SD] { + override implicit def manager(implicit + sessionConfig: SessionConfig, + companion: SessionDataCompanion[SD] + ): SessionManager[SD] = self.manager + override protected def sessionType: Session.SessionType = self.sessionType + override def log: Logger = LoggerFactory getLogger getClass.getName +// override implicit def sessionConfig: SessionConfig = self.sessionConfig + override implicit def system: ActorSystem[_] = sys + override lazy val ec: ExecutionContext = sys.executionContext + override def softPayAccountDao: SoftPayAccountDao = MockSoftPayAccountDao + override implicit def refreshTokenStorage: RefreshTokenStorage[SD] = + self.refreshTokenStorage + override implicit def companion: SessionDataCompanion[SD] = self.companion + } override def apiRoutes: ActorSystem[_] => List[ApiRoute] = - system => - List( - sessionServiceRoute(system), - paymentService(system) - ) + system => super.apiRoutes(system) :+ sessionServiceRoute(system) } diff --git a/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentTestKit.scala b/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentTestKit.scala index 7a61b24..d50519e 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentTestKit.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/scalatest/PaymentTestKit.scala @@ -1,20 +1,37 @@ package app.softnetwork.payment.scalatest import akka.actor.typed.ActorSystem +import app.softnetwork.account.config.AccountSettings +import app.softnetwork.account.model.BasicAccountProfile +import app.softnetwork.account.persistence.query.AccountEventProcessorStreams.InternalAccountEvents2AccountProcessorStream +import app.softnetwork.account.persistence.typed.AccountBehavior +import app.softnetwork.notification.scalatest.AllNotificationsTestKit +import app.softnetwork.payment.api.{ + ClientServer, + MockClientServer, + MockPaymentServer, + PaymentServer, + SoftPayClientTestKit +} import app.softnetwork.payment.config.PaymentSettings._ -import app.softnetwork.payment.handlers.MockPaymentHandler -import app.softnetwork.payment.launch.PaymentGuardian -import app.softnetwork.payment.message.PaymentMessages.{ - KycDocumentStatusNotUpdated, - UboDeclarationStatusUpdated, - _ +import app.softnetwork.payment.handlers.{ + MockPaymentHandler, + MockSoftPayAccountDao, + MockSoftPayAccountHandler, + SoftPayAccountDao } +import app.softnetwork.payment.launch.PaymentGuardian +import app.softnetwork.payment.message.PaymentMessages._ import app.softnetwork.payment.model._ import app.softnetwork.payment.persistence.query.{ - GenericPaymentCommandProcessorStream, + PaymentCommandProcessorStream, Scheduler2PaymentProcessorStream } -import app.softnetwork.payment.persistence.typed.{GenericPaymentBehavior, MockPaymentBehavior} +import app.softnetwork.payment.persistence.typed.{ + MockPaymentBehavior, + MockSoftPayAccountBehavior, + PaymentBehavior +} import app.softnetwork.persistence.launch.PersistentEntity import app.softnetwork.persistence.query.{ EventProcessorStream, @@ -23,41 +40,79 @@ import app.softnetwork.persistence.query.{ } import app.softnetwork.scheduler.config.SchedulerSettings import app.softnetwork.scheduler.scalatest.SchedulerTestKit +import com.typesafe.config.{Config, ConfigFactory} import org.scalatest.Suite import org.slf4j.{Logger, LoggerFactory} +import org.softnetwork.session.model.ApiKey -import scala.concurrent.{ExecutionContextExecutor, Future} +import scala.concurrent.{ExecutionContext, Future} -trait PaymentTestKit extends SchedulerTestKit with PaymentGuardian { _: Suite => +trait PaymentTestKit + extends SchedulerTestKit + with PaymentGuardian + with AllNotificationsTestKit + with SoftPayClientTestKit { + _: Suite => /** @return * roles associated with this node */ - override def roles: Seq[String] = super.roles :+ AkkaNodeRole + override def roles: Seq[String] = super.roles :+ AkkaNodeRole :+ AccountSettings.AkkaNodeRole + + override def paymentBehavior: ActorSystem[_] => PaymentBehavior = _ => MockPaymentBehavior + + override def accountBehavior + : ActorSystem[_] => AccountBehavior[SoftPayAccount, BasicAccountProfile] = _ => + MockSoftPayAccountBehavior - override def paymentAccountBehavior: ActorSystem[_] => GenericPaymentBehavior = _ => - MockPaymentBehavior + override def paymentServer: ActorSystem[_] => PaymentServer = system => MockPaymentServer(system) - override def paymentCommandProcessorStream - : ActorSystem[_] => GenericPaymentCommandProcessorStream = sys => - new GenericPaymentCommandProcessorStream + override def clientServer: ActorSystem[_] => ClientServer = system => MockClientServer(system) + + def loadApiKey(clientId: String): Future[Option[ApiKey]] = + MockPaymentBehavior.softPayAccountDao.loadApiKey(clientId) + + def clientId: String = provider.clientId + + override lazy val config: Config = akkaConfig + .withFallback(ConfigFactory.load("softnetwork-in-memory-persistence.conf")) + .withFallback( + ConfigFactory.parseString(softPayClientSettings) + ) + .withFallback(ConfigFactory.load()) + + override def paymentCommandProcessorStream: ActorSystem[_] => PaymentCommandProcessorStream = + sys => + new PaymentCommandProcessorStream + with MockPaymentHandler + with InMemoryJournalProvider + with InMemoryOffsetProvider { + lazy val log: Logger = LoggerFactory getLogger getClass.getName + override val forTests: Boolean = true + override implicit def system: ActorSystem[_] = sys + } + + override def scheduler2PaymentProcessorStream + : ActorSystem[_] => Scheduler2PaymentProcessorStream = sys => + new Scheduler2PaymentProcessorStream with MockPaymentHandler with InMemoryJournalProvider with InMemoryOffsetProvider { lazy val log: Logger = LoggerFactory getLogger getClass.getName + override val tag: String = SchedulerSettings.tag(MockPaymentBehavior.persistenceId) override val forTests: Boolean = true override implicit def system: ActorSystem[_] = sys } - override def scheduler2PaymentProcessorStream - : ActorSystem[_] => Scheduler2PaymentProcessorStream = sys => - new Scheduler2PaymentProcessorStream - with MockPaymentHandler + override def internalAccountEvents2AccountProcessorStream + : ActorSystem[_] => InternalAccountEvents2AccountProcessorStream = sys => + new InternalAccountEvents2AccountProcessorStream + with MockSoftPayAccountHandler with InMemoryJournalProvider with InMemoryOffsetProvider { lazy val log: Logger = LoggerFactory getLogger getClass.getName - override val tag: String = SchedulerSettings.tag(MockPaymentBehavior.persistenceId) - override val forTests: Boolean = true + override def tag: String = s"${MockSoftPayAccountBehavior.persistenceId}-to-internal" + override lazy val forTests: Boolean = true override implicit def system: ActorSystem[_] = sys } @@ -67,9 +122,8 @@ trait PaymentTestKit extends SchedulerTestKit with PaymentGuardian { _: Suite => registerCard: Boolean, printReceipt: Boolean )(implicit - system: ActorSystem[_] + ec: ExecutionContext ): Future[Either[PayInFailed, Either[PaymentRedirection, PaidIn]]] = { - implicit val ec: ExecutionContextExecutor = system.executionContext MockPaymentHandler !? PayInFor3DS(orderUuid, transactionId, registerCard, printReceipt) map { case result: PaymentRedirection => Right(Left(result)) case result: PaidIn => Right(Right(result)) @@ -85,9 +139,8 @@ trait PaymentTestKit extends SchedulerTestKit with PaymentGuardian { _: Suite => registerCard: Boolean = true, printReceipt: Boolean = false )(implicit - system: ActorSystem[_] + ec: ExecutionContext ): Future[Either[CardPreAuthorizationFailed, Either[PaymentRedirection, CardPreAuthorized]]] = { - implicit val ec: ExecutionContextExecutor = system.executionContext MockPaymentHandler !? PreAuthorizeCardFor3DS( orderUuid, preAuthorizationId, @@ -101,12 +154,12 @@ trait PaymentTestKit extends SchedulerTestKit with PaymentGuardian { _: Suite => } } - def payInFirstRecurringFor3DS(recurringPayInRegistrationId: String, transactionId: String)( - implicit system: ActorSystem[_] - ): Future[ + def payInFirstRecurringFor3DS( + recurringPayInRegistrationId: String, + transactionId: String + )(implicit ec: ExecutionContext): Future[ Either[FirstRecurringCardPaymentFailed, Either[PaymentRedirection, FirstRecurringPaidIn]] ] = { - implicit val ec: ExecutionContextExecutor = system.executionContext MockPaymentHandler !? PayInFirstRecurringFor3DS( recurringPayInRegistrationId, transactionId @@ -128,8 +181,7 @@ trait PaymentTestKit extends SchedulerTestKit with PaymentGuardian { _: Suite => def updateKycDocumentStatus( kycDocumentId: String, status: Option[KycDocument.KycDocumentStatus] = None - )(implicit system: ActorSystem[_]): Future[Either[PaymentError, KycDocumentStatusUpdated]] = { - implicit val ec: ExecutionContextExecutor = system.executionContext + )(implicit ec: ExecutionContext): Future[Either[PaymentError, KycDocumentStatusUpdated]] = { MockPaymentHandler !? UpdateKycDocumentStatus(kycDocumentId, status) map { case result: KycDocumentStatusUpdated => Right(result) case error: PaymentError => Left(error) @@ -140,18 +192,17 @@ trait PaymentTestKit extends SchedulerTestKit with PaymentGuardian { _: Suite => def updateUboDeclarationStatus( uboDeclarationId: String, status: Option[UboDeclaration.UboDeclarationStatus] = None - )(implicit system: ActorSystem[_]): Future[Boolean] = { - implicit val ec: ExecutionContextExecutor = system.executionContext + )(implicit ec: ExecutionContext): Future[Boolean] = { MockPaymentHandler !? UpdateUboDeclarationStatus(uboDeclarationId, status) map { case UboDeclarationStatusUpdated => true case _ => false } } - def updateMandateStatus(mandateId: String, status: Option[BankAccount.MandateStatus] = None)( - implicit system: ActorSystem[_] - ): Future[Either[PaymentError, MandateStatusUpdated]] = { - implicit val ec: ExecutionContextExecutor = system.executionContext + def updateMandateStatus( + mandateId: String, + status: Option[BankAccount.MandateStatus] = None + )(implicit ec: ExecutionContext): Future[Either[PaymentError, MandateStatusUpdated]] = { MockPaymentHandler !? UpdateMandateStatus(mandateId, status) map { case result: MandateStatusUpdated => Right(result) case error: PaymentError => Left(error) @@ -159,16 +210,14 @@ trait PaymentTestKit extends SchedulerTestKit with PaymentGuardian { _: Suite => } } - def validateRegularUser(userId: String)(implicit system: ActorSystem[_]): Future[Boolean] = { - implicit val ec: ExecutionContextExecutor = system.executionContext + def validateRegularUser(userId: String)(implicit ec: ExecutionContext): Future[Boolean] = { MockPaymentHandler !? ValidateRegularUser(userId) map { case RegularUserValidated => true case _ => false } } - def invalidateRegularUser(userId: String)(implicit system: ActorSystem[_]): Future[Boolean] = { - implicit val ec: ExecutionContextExecutor = system.executionContext + def invalidateRegularUser(userId: String)(implicit ec: ExecutionContext): Future[Boolean] = { MockPaymentHandler !? InvalidateRegularUser(userId) map { case RegularUserInvalidated => true case _ => false @@ -176,13 +225,21 @@ trait PaymentTestKit extends SchedulerTestKit with PaymentGuardian { _: Suite => } override def entities: ActorSystem[_] => Seq[PersistentEntity[_, _, _, _]] = sys => - schedulerEntities(sys) ++ sessionEntities(sys) ++ paymentEntities(sys) + schedulerEntities(sys) ++ sessionEntities(sys) ++ accountEntities(sys) ++ paymentEntities( + sys + ) ++ notificationEntities(sys) override def eventProcessorStreams: ActorSystem[_] => Seq[EventProcessorStream[_]] = sys => schedulerEventProcessorStreams(sys) ++ - paymentEventProcessorStreams(sys) + paymentEventProcessorStreams(sys) ++ + accountEventProcessorStreams(sys) ++ + notificationEventProcessorStreams(sys) - /*override def initSystem: ActorSystem[_] => Unit = system => { + override def initSystem: ActorSystem[_] => Unit = system => { + initAccountSystem(system) initSchedulerSystem(system) - }*/ + registerProvidersAccount(system) + } + + override def softPayAccountDao: SoftPayAccountDao = MockSoftPayAccountDao } diff --git a/testkit/src/main/scala/app/softnetwork/payment/service/MockPaymentEndpoints.scala b/testkit/src/main/scala/app/softnetwork/payment/service/MockPaymentEndpoints.scala deleted file mode 100644 index 3c1275a..0000000 --- a/testkit/src/main/scala/app/softnetwork/payment/service/MockPaymentEndpoints.scala +++ /dev/null @@ -1,33 +0,0 @@ -package app.softnetwork.payment.service - -import akka.actor.typed.ActorSystem -import app.softnetwork.api.server.SwaggerEndpoint -import app.softnetwork.payment.handlers.MockPaymentHandler -import app.softnetwork.session.service.SessionEndpoints -import org.slf4j.{Logger, LoggerFactory} - -trait MockPaymentEndpoints extends MangoPayPaymentEndpoints with MockPaymentHandler - -object MockPaymentEndpoints { - def apply( - _system: ActorSystem[_], - _sessionEndpoints: SessionEndpoints - ): MockPaymentEndpoints = { - new MockPaymentEndpoints { - lazy val log: Logger = LoggerFactory getLogger getClass.getName - override implicit def system: ActorSystem[_] = _system - override def sessionEndpoints: SessionEndpoints = _sessionEndpoints - } - } - - def swagger( - _system: ActorSystem[_], - _sessionEndpoints: SessionEndpoints - ): SwaggerEndpoint = { - new MockPaymentEndpoints with SwaggerEndpoint { - lazy val log: Logger = LoggerFactory getLogger getClass.getName - override implicit def system: ActorSystem[_] = _system - override def sessionEndpoints: SessionEndpoints = _sessionEndpoints - } - } -} diff --git a/testkit/src/main/scala/app/softnetwork/payment/service/MockPaymentService.scala b/testkit/src/main/scala/app/softnetwork/payment/service/MockPaymentService.scala index e23f71c..d737ec8 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/service/MockPaymentService.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/service/MockPaymentService.scala @@ -1,18 +1,11 @@ package app.softnetwork.payment.service -import akka.actor.typed.ActorSystem import app.softnetwork.payment.handlers.MockPaymentHandler -import app.softnetwork.session.service.SessionService -import org.slf4j.{Logger, LoggerFactory} +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials -trait MockPaymentService extends MangoPayPaymentService with MockPaymentHandler - -object MockPaymentService { - def apply(_system: ActorSystem[_], _sessionService: SessionService): MockPaymentService = { - new MockPaymentService { - lazy val log: Logger = LoggerFactory getLogger getClass.getName - override implicit def system: ActorSystem[_] = _system - override def sessionService: SessionService = _sessionService - } - } +trait MockPaymentService[SD <: SessionData with SessionDataDecorator[SD]] + extends MangoPayPaymentService[SD] + with MockPaymentHandler { + _: SessionMaterials[SD] => } diff --git a/testkit/src/main/scala/app/softnetwork/payment/service/MockPaymentServiceEndpoints.scala b/testkit/src/main/scala/app/softnetwork/payment/service/MockPaymentServiceEndpoints.scala new file mode 100644 index 0000000..d4a4dea --- /dev/null +++ b/testkit/src/main/scala/app/softnetwork/payment/service/MockPaymentServiceEndpoints.scala @@ -0,0 +1,11 @@ +package app.softnetwork.payment.service + +import app.softnetwork.payment.handlers.MockPaymentHandler +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials + +trait MockPaymentServiceEndpoints[SD <: SessionData with SessionDataDecorator[SD]] + extends MangoPayPaymentServiceEndpoints[SD] + with MockPaymentHandler { + _: SessionMaterials[SD] => +} diff --git a/testkit/src/main/scala/app/softnetwork/payment/service/MockSoftPayAccountService.scala b/testkit/src/main/scala/app/softnetwork/payment/service/MockSoftPayAccountService.scala new file mode 100644 index 0000000..08ee16a --- /dev/null +++ b/testkit/src/main/scala/app/softnetwork/payment/service/MockSoftPayAccountService.scala @@ -0,0 +1,12 @@ +package app.softnetwork.payment.service + +import app.softnetwork.payment.handlers.MockSoftPayAccountTypeKey +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials + +trait MockSoftPayAccountService[SD <: SessionData with SessionDataDecorator[SD]] + extends SoftPayAccountService[SD] + with MockSoftPayAccountTypeKey { + _: SessionMaterials[SD] => + +} diff --git a/testkit/src/main/scala/app/softnetwork/payment/service/MockSoftPayAccountServiceEndpoints.scala b/testkit/src/main/scala/app/softnetwork/payment/service/MockSoftPayAccountServiceEndpoints.scala new file mode 100644 index 0000000..dccb47f --- /dev/null +++ b/testkit/src/main/scala/app/softnetwork/payment/service/MockSoftPayAccountServiceEndpoints.scala @@ -0,0 +1,9 @@ +package app.softnetwork.payment.service + +import app.softnetwork.payment.handlers.MockSoftPayAccountTypeKey +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials + +trait MockSoftPayAccountServiceEndpoints[SD <: SessionData with SessionDataDecorator[SD]] + extends SoftPayAccountServiceEndpoints[SD] + with MockSoftPayAccountTypeKey { _: SessionMaterials[SD] => } diff --git a/testkit/src/main/scala/app/softnetwork/payment/service/MockSoftPayOAuthService.scala b/testkit/src/main/scala/app/softnetwork/payment/service/MockSoftPayOAuthService.scala new file mode 100644 index 0000000..4e37db4 --- /dev/null +++ b/testkit/src/main/scala/app/softnetwork/payment/service/MockSoftPayOAuthService.scala @@ -0,0 +1,17 @@ +package app.softnetwork.payment.service + +import app.softnetwork.account.spi.OAuth2Service +import app.softnetwork.payment.handlers.MockSoftPayAccountTypeKey +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials +import com.github.scribejava.core.oauth.DummyApiService + +trait MockSoftPayOAuthService[SD <: SessionData with SessionDataDecorator[SD]] + extends SoftPayOAuthService[SD] + with MockSoftPayAccountTypeKey { + _: SessionMaterials[SD] => + override lazy val services: Seq[OAuth2Service] = + Seq( + new DummyApiService() + ) +} diff --git a/testkit/src/main/scala/app/softnetwork/payment/service/MockSoftPayOAuthServiceEndpoints.scala b/testkit/src/main/scala/app/softnetwork/payment/service/MockSoftPayOAuthServiceEndpoints.scala new file mode 100644 index 0000000..9db8e00 --- /dev/null +++ b/testkit/src/main/scala/app/softnetwork/payment/service/MockSoftPayOAuthServiceEndpoints.scala @@ -0,0 +1,17 @@ +package app.softnetwork.payment.service + +import app.softnetwork.account.spi.OAuth2Service +import app.softnetwork.payment.handlers.MockSoftPayAccountTypeKey +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials +import com.github.scribejava.core.oauth.DummyApiService + +trait MockSoftPayOAuthServiceEndpoints[SD <: SessionData with SessionDataDecorator[SD]] + extends SoftPayOAuthServiceEndpoints[SD] + with MockSoftPayAccountTypeKey { _: SessionMaterials[SD] => + + override lazy val services: Seq[OAuth2Service] = + Seq( + new DummyApiService() + ) +} 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 d4634cc..dec6934 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/spi/MockMangoPayProvider.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/spi/MockMangoPayProvider.scala @@ -1,8 +1,11 @@ package app.softnetwork.payment.spi +import app.softnetwork.payment.config.MangoPay import app.softnetwork.payment.config.MangoPaySettings.MangoPayConfig._ -import app.softnetwork.payment.model.PaymentUser.PaymentUserType +import app.softnetwork.payment.model.NaturalUser.NaturalUserType import app.softnetwork.payment.model.RecurringPayment.RecurringCardPaymentState +import app.softnetwork.payment.model.SoftPayAccount.Client +import app.softnetwork.payment.model.SoftPayAccount.Client.Provider import app.softnetwork.payment.model.{RecurringPayment, _} import app.softnetwork.persistence._ import app.softnetwork.time.DateExtensions @@ -41,7 +44,7 @@ trait MockMangoPayProvider extends MangoPayProvider { * @return * provider user id */ - override def createOrUpdateNaturalUser(maybeNaturalUser: Option[PaymentUser]): Option[String] = + override def createOrUpdateNaturalUser(maybeNaturalUser: Option[NaturalUser]): Option[String] = maybeNaturalUser match { case Some(naturalUser) => import naturalUser._ @@ -57,10 +60,10 @@ trait MockMangoPayProvider extends MangoPayProvider { user.setTag(externalUuid) user.setNationality(CountryIso.valueOf(nationality)) user.setCountryOfResidence(CountryIso.valueOf(countryOfResidence)) - paymentUserType match { + naturalUserType match { case Some(value) => value match { - case PaymentUserType.PAYER => user.setUserCategory(UserCategory.PAYER) + case NaturalUserType.PAYER => user.setUserCategory(UserCategory.PAYER) case _ => user.setUserCategory(UserCategory.OWNER) user.setTermsAndConditionsAccepted(true) @@ -126,10 +129,10 @@ trait MockMangoPayProvider extends MangoPayProvider { ) user.setEmail(legalRepresentative.email) user.setCompanyNumber(siret) - legalRepresentative.paymentUserType match { + legalRepresentative.naturalUserType match { case Some(value) => value match { - case PaymentUserType.PAYER => user.setUserCategory(UserCategory.PAYER) + case NaturalUserType.PAYER => user.setUserCategory(UserCategory.PAYER) case _ => user.setUserCategory(UserCategory.OWNER) user.setTermsAndConditionsAccepted(true) @@ -1236,6 +1239,13 @@ trait MockMangoPayProvider extends MangoPayProvider { } } + override def client: Option[SoftPayAccount.Client] = + Some( + SoftPayAccount.Client.defaultInstance + .withProvider(provider) + .withClientId(provider.clientId) + ) + /** @return * client fees */ @@ -1710,3 +1720,16 @@ case class RecurringCardPaymentRegistration( currentState: RecurringCardPaymentState, registration: CreateRecurringPayment ) + +class MockMangoPayProviderFactory extends PaymentProviderSpi { + override val providerType: Provider.ProviderType = + Provider.ProviderType.MOCK + + override def paymentProvider(p: Client.Provider): MockMangoPayProvider = + new MockMangoPayProvider { + override implicit def provider: Provider = p + } + + override def softPaymentProvider: Provider = + MangoPay.softPayProvider.withProviderType(providerType) +} 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 4b9e2ab..cda60ad 100644 --- a/testkit/src/test/scala/app/softnetwork/payment/handlers/PaymentHandlerSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/payment/handlers/PaymentHandlerSpec.scala @@ -1,7 +1,7 @@ package app.softnetwork.payment.handlers import akka.actor.typed.ActorSystem -import app.softnetwork.payment.api.{PaymentClient, PaymentGrpcServer} +import app.softnetwork.payment.api.{PaymentClient, PaymentGrpcServerTestKit} import app.softnetwork.payment.data._ import app.softnetwork.payment.message.PaymentMessages._ import app.softnetwork.payment.model.PaymentAccount.User @@ -9,8 +9,10 @@ import app.softnetwork.payment.model._ import app.softnetwork.payment.scalatest.PaymentTestKit import app.softnetwork.time._ import app.softnetwork.persistence.now +import app.softnetwork.session.config.Settings import org.scalatest.wordspec.AnyWordSpecLike import org.slf4j.{Logger, LoggerFactory} +import org.softnetwork.session.model.Session import java.time.LocalDate import scala.language.implicitConversions @@ -19,21 +21,25 @@ import scala.util.{Failure, Success} class PaymentHandlerSpec extends MockPaymentHandler with AnyWordSpecLike - with PaymentGrpcServer - with PaymentTestKit { + with PaymentTestKit + with PaymentGrpcServerTestKit { lazy val log: Logger = LoggerFactory getLogger getClass.getName - implicit lazy val system: ActorSystem[_] = typedSystem() + implicit lazy val ts: ActorSystem[_] = typedSystem() - lazy val client: PaymentClient = PaymentClient(system) + override protected def sessionType: Session.SessionType = + Settings.Session.SessionContinuityAndTransport + + lazy val paymentClient: PaymentClient = PaymentClient(ts) "Payment handler" must { "pre register card" in { !?( PreRegisterCard( orderUuid, - naturalUser.withProfile("customer") + naturalUser.withProfile("customer"), + clientId = Some(clientId) ) ) await { case cardPreRegistered: CardPreRegistered => @@ -52,7 +58,7 @@ class PaymentHandlerSpec assert(naturalUser.userId.isDefined) assert(naturalUser.walletId.isDefined) assert( - naturalUser.paymentUserType.getOrElse(PaymentUser.PaymentUserType.COLLECTOR).isPayer + naturalUser.naturalUserType.getOrElse(NaturalUser.NaturalUserType.COLLECTOR).isPayer ) case other => fail(other.toString) } @@ -153,7 +159,8 @@ class PaymentHandlerSpec CreateOrUpdateBankAccount( computeExternalUuidWithProfile(sellerUuid, Some("seller")), BankAccount(None, ownerName, ownerAddress, iban, ""), - Some(User.NaturalUser(naturalUser.withExternalUuid(sellerUuid).withProfile("seller"))) + Some(User.NaturalUser(naturalUser.withExternalUuid(sellerUuid).withProfile("seller"))), + clientId = Some(clientId) ) ) await { case r: BankAccountCreatedOrUpdated => @@ -170,8 +177,8 @@ class PaymentHandlerSpec ) sellerBankAccountId = paymentAccount.bankAccount.flatMap(_.id).getOrElse("") assert( - paymentAccount.getNaturalUser.paymentUserType - .getOrElse(PaymentUser.PaymentUserType.PAYER) + paymentAccount.getNaturalUser.naturalUserType + .getOrElse(NaturalUser.NaturalUserType.PAYER) .isCollector ) case other => fail(other.toString) @@ -199,7 +206,8 @@ class PaymentHandlerSpec .withExternalUuid(sellerUuid) .withProfile("seller") ) - ) + ), + clientId = Some(clientId) ) ) await { case r: BankAccountCreatedOrUpdated => @@ -218,8 +226,8 @@ class PaymentHandlerSpec sellerBankAccountId = paymentAccount.bankAccount.flatMap(_.id).getOrElse("") // assert(sellerBankAccountId != previousBankAccountId) assert( - paymentAccount.getNaturalUser.paymentUserType - .getOrElse(PaymentUser.PaymentUserType.PAYER) + paymentAccount.getNaturalUser.naturalUserType + .getOrElse(NaturalUser.NaturalUserType.PAYER) .isCollector ) case other => fail(other.toString) @@ -245,7 +253,8 @@ class PaymentHandlerSpec .withExternalUuid(sellerUuid) .withProfile("seller") ) - ) + ), + clientId = Some(clientId) ) ) await { case r: BankAccountCreatedOrUpdated => @@ -272,7 +281,8 @@ class PaymentHandlerSpec .withExternalUuid(sellerUuid) .withProfile("seller") ) - ) + ), + clientId = Some(clientId) ) ) await { case r: BankAccountCreatedOrUpdated => @@ -303,7 +313,8 @@ class PaymentHandlerSpec .withExternalUuid(sellerUuid) .withProfile("seller") ) - ) + ), + clientId = Some(clientId) ) ) await { case r: BankAccountCreatedOrUpdated => assert(!r.kycUpdated && !r.documentsUpdated && r.userUpdated) @@ -330,7 +341,8 @@ class PaymentHandlerSpec .withExternalUuid(sellerUuid) .withProfile("seller") ) - ) + ), + clientId = Some(clientId) ) ) await { case r: BankAccountCreatedOrUpdated => assert(!r.kycUpdated && !r.documentsUpdated && r.userUpdated) @@ -410,7 +422,8 @@ class PaymentHandlerSpec legalUser.withLegalRepresentative(legalUser.legalRepresentative.withProfile("seller")) ) ), - Some(true) + Some(true), + clientId = Some(clientId) ) ) await { case r: BankAccountCreatedOrUpdated => @@ -460,7 +473,8 @@ class PaymentHandlerSpec ) ) ), - Some(true) + Some(true), + clientId = Some(clientId) ) ) await { case r: BankAccountCreatedOrUpdated => @@ -496,7 +510,7 @@ class PaymentHandlerSpec sellerBankAccountId = paymentAccount.bankAccount.flatMap(_.id).getOrElse("") // assert(sellerBankAccountId != previousBankAccountId) uboDeclarationId = paymentAccount.getLegalUser.uboDeclaration.map(_.id).getOrElse("") - client.loadLegalUserDetails( + paymentClient.loadLegalUserDetails( computeExternalUuidWithProfile(sellerUuid, Some("seller")) ) complete () match { case Success(value) => @@ -536,7 +550,8 @@ class PaymentHandlerSpec computeExternalUuidWithProfile(sellerUuid, Some("seller")), updatedBankAccount, Some(User.LegalUser(updatedLegalUser)), - Some(true) + Some(true), + clientId = Some(clientId) ) ) await { case r: BankAccountCreatedOrUpdated => @@ -585,7 +600,8 @@ class PaymentHandlerSpec computeExternalUuidWithProfile(sellerUuid, Some("seller")), updatedBankAccount, Some(User.LegalUser(updatedLegalUser)), - Some(true) + Some(true), + clientId = Some(clientId) ) ) await { case r: BankAccountCreatedOrUpdated => @@ -600,7 +616,8 @@ class PaymentHandlerSpec computeExternalUuidWithProfile(sellerUuid, Some("seller")), updatedBankAccount.withBic(""), Some(User.LegalUser(updatedLegalUser)), - Some(true) + Some(true), + clientId = Some(clientId) ) ) await { case r: BankAccountCreatedOrUpdated => @@ -619,7 +636,8 @@ class PaymentHandlerSpec computeExternalUuidWithProfile(sellerUuid, Some("seller")), updatedBankAccount, Some(User.LegalUser(updatedLegalUser)), - Some(true) + Some(true), + clientId = Some(clientId) ) ) await { case r: BankAccountCreatedOrUpdated => @@ -789,7 +807,7 @@ class PaymentHandlerSpec case result: CardPreAuthorized => val transactionId = result.transactionId preAuthorizationId = transactionId - client.payInWithCardPreAuthorized( + paymentClient.payInWithCardPreAuthorized( preAuthorizationId, computeExternalUuidWithProfile(sellerUuid, Some("seller")), Some(110) @@ -799,7 +817,7 @@ class PaymentHandlerSpec assert(result.error.getOrElse("") == "DebitedAmountAbovePreAuthorizationAmount") case Failure(f) => fail(f.getMessage) } - client.payInWithCardPreAuthorized( + paymentClient.payInWithCardPreAuthorized( preAuthorizationId, computeExternalUuidWithProfile(sellerUuid, Some("seller")), Some(90) @@ -820,7 +838,7 @@ class PaymentHandlerSpec ) case other => fail(other.getClass.toString) } - client.payOut( + paymentClient.payOut( orderUuid, computeExternalUuidWithProfile(sellerUuid, Some("seller")), 100, @@ -850,7 +868,7 @@ class PaymentHandlerSpec ) ) await { case _: PaidIn => - client.payOut( + paymentClient.payOut( orderUuid, computeExternalUuidWithProfile(sellerUuid, Some("seller")), 100, @@ -898,7 +916,7 @@ class PaymentHandlerSpec ) ) await { case _: PaidIn => - client.payOut( + paymentClient.payOut( orderUuid, computeExternalUuidWithProfile(sellerUuid, Some("seller")), 100, @@ -929,7 +947,7 @@ class PaymentHandlerSpec ) await { case result: PaidIn => val payInTransactionId = result.transactionId - client.refund( + paymentClient.refund( orderUuid, payInTransactionId, 101, @@ -940,7 +958,7 @@ class PaymentHandlerSpec case Success(r) => assert(r.transactionId.isEmpty) assert(r.error.getOrElse("") == "IllegalTransactionAmount") - client.refund( + paymentClient.refund( orderUuid, payInTransactionId, 50, @@ -964,7 +982,8 @@ class PaymentHandlerSpec CreateOrUpdateBankAccount( computeExternalUuidWithProfile(vendorUuid, Some("vendor")), BankAccount(None, ownerName, ownerAddress, iban, bic), - Some(User.NaturalUser(naturalUser.withExternalUuid(vendorUuid).withProfile("vendor"))) + Some(User.NaturalUser(naturalUser.withExternalUuid(vendorUuid).withProfile("vendor"))), + clientId = Some(clientId) ) ) await { case r: BankAccountCreatedOrUpdated => @@ -999,7 +1018,7 @@ class PaymentHandlerSpec } case other => fail(other.toString) } - client.transfer( + paymentClient.transfer( Some(orderUuid), computeExternalUuidWithProfile(sellerUuid, Some("seller")), computeExternalUuidWithProfile(vendorUuid, Some("vendor")), @@ -1036,7 +1055,7 @@ class PaymentHandlerSpec } "direct debit" in { - client.directDebit( + paymentClient.directDebit( computeExternalUuidWithProfile(vendorUuid, Some("vendor")), 100, 0, diff --git a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithOneOffCookieSessionSpec.scala b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithOneOffCookieSessionSpec.scala index 667032c..6a6c9e9 100644 --- a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithOneOffCookieSessionSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithOneOffCookieSessionSpec.scala @@ -3,9 +3,15 @@ package app.softnetwork.payment.service import app.softnetwork.payment.scalatest.PaymentEndpointsTestKit import app.softnetwork.session.scalatest.OneOffCookieSessionEndpointsTestKit import app.softnetwork.session.CsrfCheckHeader +import app.softnetwork.session.model.SessionDataCompanion +import app.softnetwork.session.service.JwtClaimsSessionMaterials +import org.softnetwork.session.model.JwtClaims class PaymentEndpointsWithOneOffCookieSessionSpec - extends PaymentServiceSpec - with OneOffCookieSessionEndpointsTestKit - with PaymentEndpointsTestKit + extends PaymentServiceSpec[JwtClaims] + with OneOffCookieSessionEndpointsTestKit[JwtClaims] + with PaymentEndpointsTestKit[JwtClaims] with CsrfCheckHeader + with JwtClaimsSessionMaterials { + override implicit def companion: SessionDataCompanion[JwtClaims] = JwtClaims +} diff --git a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithOneOffHeaderSessionSpec.scala b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithOneOffHeaderSessionSpec.scala index 05195d1..ed81055 100644 --- a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithOneOffHeaderSessionSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithOneOffHeaderSessionSpec.scala @@ -3,9 +3,15 @@ package app.softnetwork.payment.service import app.softnetwork.payment.scalatest.PaymentEndpointsTestKit import app.softnetwork.session.scalatest.OneOffHeaderSessionEndpointsTestKit import app.softnetwork.session.CsrfCheckHeader +import app.softnetwork.session.model.SessionDataCompanion +import app.softnetwork.session.service.JwtClaimsSessionMaterials +import org.softnetwork.session.model.JwtClaims class PaymentEndpointsWithOneOffHeaderSessionSpec - extends PaymentServiceSpec - with OneOffHeaderSessionEndpointsTestKit - with PaymentEndpointsTestKit + extends PaymentServiceSpec[JwtClaims] + with OneOffHeaderSessionEndpointsTestKit[JwtClaims] + with PaymentEndpointsTestKit[JwtClaims] with CsrfCheckHeader + with JwtClaimsSessionMaterials { + override implicit def companion: SessionDataCompanion[JwtClaims] = JwtClaims +} diff --git a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithRefreshableCookieSessionSpec.scala b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithRefreshableCookieSessionSpec.scala index 13fe4e5..00fc3f9 100644 --- a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithRefreshableCookieSessionSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithRefreshableCookieSessionSpec.scala @@ -3,9 +3,15 @@ package app.softnetwork.payment.service import app.softnetwork.payment.scalatest.PaymentEndpointsTestKit import app.softnetwork.session.scalatest.RefreshableCookieSessionEndpointsTestKit import app.softnetwork.session.CsrfCheckHeader +import app.softnetwork.session.model.SessionDataCompanion +import app.softnetwork.session.service.JwtClaimsSessionMaterials +import org.softnetwork.session.model.JwtClaims class PaymentEndpointsWithRefreshableCookieSessionSpec - extends PaymentServiceSpec - with RefreshableCookieSessionEndpointsTestKit - with PaymentEndpointsTestKit + extends PaymentServiceSpec[JwtClaims] + with RefreshableCookieSessionEndpointsTestKit[JwtClaims] + with PaymentEndpointsTestKit[JwtClaims] with CsrfCheckHeader + with JwtClaimsSessionMaterials { + override implicit def companion: SessionDataCompanion[JwtClaims] = JwtClaims +} diff --git a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithRefreshableHeaderSessionSpec.scala b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithRefreshableHeaderSessionSpec.scala index 483cbb4..029388e 100644 --- a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithRefreshableHeaderSessionSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentEndpointsWithRefreshableHeaderSessionSpec.scala @@ -3,9 +3,15 @@ package app.softnetwork.payment.service import app.softnetwork.payment.scalatest.PaymentEndpointsTestKit import app.softnetwork.session.scalatest.RefreshableHeaderSessionEndpointsTestKit import app.softnetwork.session.CsrfCheckHeader +import app.softnetwork.session.model.SessionDataCompanion +import app.softnetwork.session.service.JwtClaimsSessionMaterials +import org.softnetwork.session.model.JwtClaims class PaymentEndpointsWithRefreshableHeaderSessionSpec - extends PaymentServiceSpec - with RefreshableHeaderSessionEndpointsTestKit - with PaymentEndpointsTestKit + extends PaymentServiceSpec[JwtClaims] + with RefreshableHeaderSessionEndpointsTestKit[JwtClaims] + with PaymentEndpointsTestKit[JwtClaims] with CsrfCheckHeader + with JwtClaimsSessionMaterials { + override implicit def companion: SessionDataCompanion[JwtClaims] = JwtClaims +} diff --git a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithOneOffCookieSessionSpec.scala b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithOneOffCookieSessionSpec.scala index aacfc5c..3ed8baa 100644 --- a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithOneOffCookieSessionSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithOneOffCookieSessionSpec.scala @@ -1,9 +1,15 @@ package app.softnetwork.payment.service import app.softnetwork.payment.scalatest.PaymentRoutesTestKit +import app.softnetwork.session.model.SessionDataCompanion import app.softnetwork.session.scalatest.OneOffCookieSessionServiceTestKit +import app.softnetwork.session.service.JwtClaimsSessionMaterials +import org.softnetwork.session.model.JwtClaims class PaymentRoutesWithOneOffCookieSessionSpec - extends PaymentServiceSpec - with OneOffCookieSessionServiceTestKit - with PaymentRoutesTestKit + extends PaymentServiceSpec[JwtClaims] + with OneOffCookieSessionServiceTestKit[JwtClaims] + with PaymentRoutesTestKit[JwtClaims] + with JwtClaimsSessionMaterials { + override implicit def companion: SessionDataCompanion[JwtClaims] = JwtClaims +} diff --git a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithOneOffHeaderSessionSpec.scala b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithOneOffHeaderSessionSpec.scala index 0fb196b..b5d8671 100644 --- a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithOneOffHeaderSessionSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithOneOffHeaderSessionSpec.scala @@ -1,9 +1,15 @@ package app.softnetwork.payment.service import app.softnetwork.payment.scalatest.PaymentRoutesTestKit +import app.softnetwork.session.model.SessionDataCompanion import app.softnetwork.session.scalatest.OneOffHeaderSessionServiceTestKit +import app.softnetwork.session.service.JwtClaimsSessionMaterials +import org.softnetwork.session.model.JwtClaims class PaymentRoutesWithOneOffHeaderSessionSpec - extends PaymentServiceSpec - with OneOffHeaderSessionServiceTestKit - with PaymentRoutesTestKit + extends PaymentServiceSpec[JwtClaims] + with OneOffHeaderSessionServiceTestKit[JwtClaims] + with PaymentRoutesTestKit[JwtClaims] + with JwtClaimsSessionMaterials { + override implicit def companion: SessionDataCompanion[JwtClaims] = JwtClaims +} diff --git a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithRefreshableCookieSessionSpec.scala b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithRefreshableCookieSessionSpec.scala index 4b1857c..144a98b 100644 --- a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithRefreshableCookieSessionSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithRefreshableCookieSessionSpec.scala @@ -1,9 +1,15 @@ package app.softnetwork.payment.service import app.softnetwork.payment.scalatest.PaymentRoutesTestKit +import app.softnetwork.session.model.SessionDataCompanion import app.softnetwork.session.scalatest.RefreshableCookieSessionServiceTestKit +import app.softnetwork.session.service.JwtClaimsSessionMaterials +import org.softnetwork.session.model.JwtClaims class PaymentRoutesWithRefreshableCookieSessionSpec - extends PaymentServiceSpec - with RefreshableCookieSessionServiceTestKit - with PaymentRoutesTestKit + extends PaymentServiceSpec[JwtClaims] + with RefreshableCookieSessionServiceTestKit[JwtClaims] + with PaymentRoutesTestKit[JwtClaims] + with JwtClaimsSessionMaterials { + override implicit def companion: SessionDataCompanion[JwtClaims] = JwtClaims +} diff --git a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithRefreshableHeaderSessionSpec.scala b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithRefreshableHeaderSessionSpec.scala index 447746d..3986808 100644 --- a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithRefreshableHeaderSessionSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentRoutesWithRefreshableHeaderSessionSpec.scala @@ -1,9 +1,15 @@ package app.softnetwork.payment.service import app.softnetwork.payment.scalatest.PaymentRoutesTestKit +import app.softnetwork.session.model.SessionDataCompanion import app.softnetwork.session.scalatest.RefreshableHeaderSessionServiceTestKit +import app.softnetwork.session.service.JwtClaimsSessionMaterials +import org.softnetwork.session.model.JwtClaims class PaymentRoutesWithRefreshableHeaderSessionSpec - extends PaymentServiceSpec - with RefreshableHeaderSessionServiceTestKit - with PaymentRoutesTestKit + extends PaymentServiceSpec[JwtClaims] + with RefreshableHeaderSessionServiceTestKit[JwtClaims] + with PaymentRoutesTestKit[JwtClaims] + with JwtClaimsSessionMaterials { + override implicit def companion: SessionDataCompanion[JwtClaims] = JwtClaims +} diff --git a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentServiceSpec.scala b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentServiceSpec.scala index f7533ea..c0c8729 100644 --- a/testkit/src/test/scala/app/softnetwork/payment/service/PaymentServiceSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/payment/service/PaymentServiceSpec.scala @@ -12,6 +12,8 @@ import app.softnetwork.payment.model._ import app.softnetwork.payment.scalatest.PaymentRouteTestKit import app.softnetwork.time._ import app.softnetwork.persistence.now +import app.softnetwork.session.model.{SessionData, SessionDataDecorator} +import app.softnetwork.session.service.SessionMaterials import org.scalatest.wordspec.AnyWordSpecLike import org.slf4j.{Logger, LoggerFactory} @@ -20,17 +22,26 @@ import java.time.LocalDate import scala.language.implicitConversions import scala.util.{Failure, Success} -trait PaymentServiceSpec extends AnyWordSpecLike with PaymentRouteTestKit { _: ApiRoutes => +trait PaymentServiceSpec[SD <: SessionData with SessionDataDecorator[SD]] + extends AnyWordSpecLike + with PaymentRouteTestKit[SD] { + _: ApiRoutes with SessionMaterials[SD] => lazy val log: Logger = LoggerFactory getLogger getClass.getName import app.softnetwork.serialization._ - lazy val client: PaymentClient = PaymentClient(typedSystem()) + lazy val paymentClient: PaymentClient = PaymentClient(ts) + + lazy val customerSession: SD with SessionDataDecorator[SD] = + companion.newSession.withId(customerUuid).withProfile(Some("customer")).withClientId(clientId) + + lazy val sellerSession: SD with SessionDataDecorator[SD] = + companion.newSession.withId(sellerUuid).withProfile(Some("seller")).withClientId(clientId) "Payment service" must { "pre register card" in { - createSession(customerUuid, Some("customer")) + createNewSession(customerSession) withHeaders( Get(s"/$RootPath/$PaymentPath/$CardRoute") ) ~> routes ~> check { @@ -103,7 +114,7 @@ trait PaymentServiceSpec extends AnyWordSpecLike with PaymentRouteTestKit { _: A } "not create bank account with wrong iban" in { - createSession(sellerUuid, Some("seller")) + createNewSession(sellerSession) withHeaders( Post( s"/$RootPath/$PaymentPath/$BankRoute", @@ -329,7 +340,7 @@ trait PaymentServiceSpec extends AnyWordSpecLike with PaymentRouteTestKit { _: A } "pay in / out with pre authorized card" in { - createSession(customerUuid, Some("customer")) + createNewSession(customerSession) withHeaders( Post( s"/$RootPath/$PaymentPath/$PreAuthorizeCardRoute", @@ -345,7 +356,7 @@ trait PaymentServiceSpec extends AnyWordSpecLike with PaymentRouteTestKit { _: A ) ~> routes ~> check { status shouldEqual StatusCodes.OK preAuthorizationId = responseAs[CardPreAuthorized].transactionId - client.payInWithCardPreAuthorized( + paymentClient.payInWithCardPreAuthorized( preAuthorizationId, computeExternalUuidWithProfile(sellerUuid, Some("seller")), None @@ -353,7 +364,7 @@ trait PaymentServiceSpec extends AnyWordSpecLike with PaymentRouteTestKit { _: A case Success(result) => assert(result.transactionId.isDefined) assert(result.error.isEmpty) - client.payOut( + paymentClient.payOut( orderUuid, computeExternalUuidWithProfile(sellerUuid, Some("seller")), 100, @@ -372,7 +383,7 @@ trait PaymentServiceSpec extends AnyWordSpecLike with PaymentRouteTestKit { _: A } "pay in / out with 3ds" in { - createSession(customerUuid, Some("customer")) + createNewSession(customerSession) withHeaders( Post( s"/$RootPath/$PaymentPath/$PayInRoute/${URLEncoder @@ -407,7 +418,7 @@ trait PaymentServiceSpec extends AnyWordSpecLike with PaymentRouteTestKit { _: A ) ~> routes ~> check { status shouldEqual StatusCodes.OK assert(responseAs[PaidIn].transactionId == transactionId) - client.payOut( + paymentClient.payOut( orderUuid, computeExternalUuidWithProfile(sellerUuid, Some("seller")), 5100, @@ -425,7 +436,7 @@ trait PaymentServiceSpec extends AnyWordSpecLike with PaymentRouteTestKit { _: A } "pay in / out with PayPal" in { - createSession(customerUuid, Some("customer")) + createNewSession(customerSession) withHeaders( Post( s"/$RootPath/$PaymentPath/$PayInRoute/${URLEncoder @@ -456,7 +467,7 @@ trait PaymentServiceSpec extends AnyWordSpecLike with PaymentRouteTestKit { _: A ) ~> routes ~> check { status shouldEqual StatusCodes.OK assert(responseAs[PaidIn].transactionId == transactionId) - client.payOut( + paymentClient.payOut( orderUuid, computeExternalUuidWithProfile(sellerUuid, Some("seller")), 5100, @@ -474,7 +485,7 @@ trait PaymentServiceSpec extends AnyWordSpecLike with PaymentRouteTestKit { _: A } "create mandate" in { - createSession(sellerUuid, Some("seller")) + createNewSession(sellerSession) withHeaders( Post(s"/$RootPath/$PaymentPath/$MandateRoute") ) ~> routes ~> check { @@ -553,7 +564,7 @@ trait PaymentServiceSpec extends AnyWordSpecLike with PaymentRouteTestKit { _: A } "register recurring card payment" in { - createSession(customerUuid, Some("customer")) + createNewSession(customerSession) withHeaders( Post( s"/$RootPath/$PaymentPath/$RecurringPaymentRoute", @@ -630,7 +641,7 @@ trait PaymentServiceSpec extends AnyWordSpecLike with PaymentRouteTestKit { _: A } "cancel mandate" in { - createSession(sellerUuid, Some("seller")) + createNewSession(sellerSession) withHeaders( Delete(s"/$RootPath/$PaymentPath/$MandateRoute") ) ~> routes ~> check { @@ -662,7 +673,7 @@ trait PaymentServiceSpec extends AnyWordSpecLike with PaymentRouteTestKit { _: A } "disable card" in { - createSession(customerUuid, Some("customer")) + createNewSession(customerSession) withHeaders( Delete(s"/$RootPath/$PaymentPath/$CardRoute?cardId=$cardId") ) ~> routes ~> check {