diff --git a/client/src/main/protobuf/model/payment/transaction.proto b/client/src/main/protobuf/model/payment/transaction.proto index 5a51f9d..bb31a51 100644 --- a/client/src/main/protobuf/model/payment/transaction.proto +++ b/client/src/main/protobuf/model/payment/transaction.proto @@ -253,7 +253,7 @@ message CardPreRegistration{ message RecurringPaymentTransaction{ option (scalapb.message).extends = "ProtobufDomainObject"; - required string recurringPayInRegistrationId = 1; + required string recurringPaymentRegistrationId = 1; required string statementDescriptor = 2; required int32 debitedAmount = 3; required int32 feesAmount = 4; 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 cc7ec92..5bb8519 100644 --- a/common/src/main/scala/app/softnetwork/payment/message/PaymentMessages.scala +++ b/common/src/main/scala/app/softnetwork/payment/message/PaymentMessages.scala @@ -19,8 +19,12 @@ object PaymentMessages { def key: String } + trait PaymentAccountCommand extends PaymentCommand + /** Payment Commands */ + trait CardCommand extends PaymentCommand + /** @param orderUuid * - order uuid * @param user @@ -35,7 +39,8 @@ object PaymentMessages { user: NaturalUser, currency: String = "EUR", clientId: Option[String] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with CardCommand { val key: String = user.externalUuidWithProfile } @@ -89,7 +94,7 @@ object PaymentMessages { user: Option[NaturalUser] = None ) - /** Flow [PreRegisterCard -> ] PreAuthorizeCard [ -> PreAuthorizeCardFor3DS] + /** Flow [PreRegisterCard -> ] PreAuthorizeCard [ -> PreAuthorizeCardCallback] * * @param orderUuid * - order uuid @@ -129,7 +134,8 @@ object PaymentMessages { printReceipt: Boolean = false, creditedAccount: Option[String] = None, feesAmount: Option[Int] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with CardCommand { val key: String = debitedAccount } @@ -150,7 +156,8 @@ object PaymentMessages { preAuthorizationId: String, registerCard: Boolean = true, printReceipt: Boolean = false - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with CardCommand { lazy val key: String = preAuthorizationId } @@ -165,7 +172,8 @@ object PaymentMessages { orderUuid: String, cardPreAuthorizedTransactionId: String, clientId: Option[String] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with CardCommand { lazy val key: String = cardPreAuthorizedTransactionId } @@ -177,11 +185,14 @@ object PaymentMessages { case class ValidatePreAuthorization( orderUuid: String, cardPreAuthorizedTransactionId: String - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with CardCommand { lazy val key: String = cardPreAuthorizedTransactionId } - /** Flow [PreRegisterCard -> ] PreAuthorizeCard [ -> PreAuthorizeCardFor3DS] -> + trait PayInCommand extends PaymentCommand + + /** Flow [PreRegisterCard -> ] PreAuthorizeCard [ -> PreAuthorizeCardCallback] -> * PayInWithCardPreAuthorized * * @param preAuthorizationId @@ -199,11 +210,13 @@ object PaymentMessages { creditedAccount: String, debitedAmount: Option[Int], clientId: Option[String] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PayInCommand + with CardCommand { lazy val key: String = preAuthorizationId } - /** Flow [PreRegisterCard ->] PayIn [ -> PayInFor3DS | PayInForPayPal] + /** Flow [PreRegisterCard ->] PayIn [ -> PayInCallback] * * @param orderUuid * - order uuid @@ -251,7 +264,8 @@ object PaymentMessages { feesAmount: Option[Int] = None, user: Option[NaturalUser] = None, clientId: Option[String] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PayInCommand { val key: String = debitedAccount } @@ -272,10 +286,13 @@ object PaymentMessages { transactionId: String, registerCard: Boolean, printReceipt: Boolean = false - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PayInCommand { lazy val key: String = transactionId } + trait PayOutCommand extends PaymentCommand + /** @param orderUuid * - order uuid * @param creditedAccount @@ -302,7 +319,8 @@ object PaymentMessages { externalReference: Option[String] = None, payInTransactionId: Option[String] = None, //TODO should be required clientId: Option[String] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PayOutCommand { val key: String = creditedAccount } @@ -310,7 +328,8 @@ object PaymentMessages { orderUuid: String, transactionId: String, clientId: Option[String] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PayOutCommand { val key: String = transactionId } @@ -318,7 +337,8 @@ object PaymentMessages { orderUuid: String, transactionId: String, clientId: Option[String] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PayInCommand { val key: String = transactionId } @@ -421,6 +441,8 @@ object PaymentMessages { val key: String = directDebitTransactionId } + trait RecurringPaymentCommand extends PaymentCommand + /** @param debitedAccount * - account to debit * @param firstDebitedAmount @@ -465,12 +487,13 @@ object PaymentMessages { statementDescriptor: Option[String] = None, externalReference: Option[String] = None, clientId: Option[String] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with RecurringPaymentCommand { val key: String = debitedAccount } - /** @param recurringPayInRegistrationId - * - recurring payIn registration id + /** @param recurringPaymentRegistrationId + * - recurring payment registration id * @param cardId * - card id * @param status @@ -478,10 +501,11 @@ object PaymentMessages { */ case class UpdateRecurringCardPaymentRegistration( debitedAccount: String, - recurringPayInRegistrationId: String, + recurringPaymentRegistrationId: String, cardId: Option[String] = None, status: Option[RecurringPayment.RecurringCardPaymentStatus] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with RecurringPaymentCommand { val key: String = debitedAccount } @@ -489,12 +513,13 @@ object PaymentMessages { * - recurring payment registration id */ case class LoadRecurringPayment(debitedAccount: String, recurringPaymentRegistrationId: String) - extends PaymentCommandWithKey { + extends PaymentCommandWithKey + with RecurringPaymentCommand { val key: String = debitedAccount } - /** @param recurringPayInRegistrationId - * - recurring payIn registration id + /** @param recurringPaymentRegistrationId + * - recurring payment registration id * @param debitedAccount * - debited account * @param ipAddress @@ -504,13 +529,14 @@ object PaymentMessages { * @param statementDescriptor * - statement descriptor */ - case class PayInFirstRecurring( - recurringPayInRegistrationId: String, + case class ExecuteFirstRecurringPayment( + recurringPaymentRegistrationId: String, debitedAccount: String, ipAddress: Option[String] = None, browserInfo: Option[BrowserInfo] = None, statementDescriptor: Option[String] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with RecurringPaymentCommand { val key: String = debitedAccount } @@ -523,7 +549,8 @@ object PaymentMessages { private[payment] case class FirstRecurringPaymentCallback( recurringPayInRegistrationId: String, transactionId: String - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with RecurringPaymentCommand { lazy val key: String = transactionId } @@ -538,13 +565,14 @@ object PaymentMessages { * @param statementDescriptor * - statement descriptor */ - case class PayNextRecurring( + case class ExecuteNextRecurringPayment( recurringPaymentRegistrationId: String, debitedAccount: String, nextDebitedAmount: Option[Int] = None, nextFeesAmount: Option[Int] = None, statementDescriptor: Option[String] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with RecurringPaymentCommand { val key: String = debitedAccount } @@ -563,14 +591,17 @@ object PaymentMessages { */ @InternalApi private[payment] case class LoadPaymentAccount(account: String, clientId: Option[String] = None) - extends PaymentCommandWithKey { + extends PaymentCommandWithKey + with PaymentAccountCommand { lazy val key: String = account } /** @param transactionId * - transaction id */ - case class LoadTransaction(transactionId: String) extends PaymentCommandWithKey { + case class LoadTransaction(transactionId: String) + extends PaymentCommandWithKey + with PaymentAccountCommand { lazy val key: String = transactionId } @@ -620,7 +651,8 @@ object PaymentMessages { ipAddress: Option[String] = None, userAgent: Option[String], clientId: Option[String] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PaymentAccountCommand { val key: String = creditedAccount } @@ -630,7 +662,8 @@ object PaymentMessages { * - optional client id */ case class LoadBankAccount(creditedAccount: String, clientId: Option[String] = None) - extends PaymentCommandWithKey { + extends PaymentCommandWithKey + with PaymentAccountCommand { val key: String = creditedAccount } @@ -643,14 +676,15 @@ object PaymentMessages { case class DeleteBankAccount( creditedAccount: String, force: Option[Boolean] - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PaymentAccountCommand { val key: String = creditedAccount } /** @param debitedAccount * - account owning the cards to load */ - case class LoadCards(debitedAccount: String) extends PaymentCommandWithKey { + case class LoadCards(debitedAccount: String) extends PaymentCommandWithKey with CardCommand { val key: String = debitedAccount } @@ -659,7 +693,9 @@ object PaymentMessages { * @param cardId * - card id */ - case class DisableCard(debitedAccount: String, cardId: String) extends PaymentCommandWithKey { + case class DisableCard(debitedAccount: String, cardId: String) + extends PaymentCommandWithKey + with CardCommand { val key: String = debitedAccount } @@ -674,7 +710,8 @@ object PaymentMessages { creditedAccount: String, pages: Seq[Array[Byte]], kycDocumentType: KycDocument.KycDocumentType - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PaymentAccountCommand { val key: String = creditedAccount } @@ -682,7 +719,8 @@ object PaymentMessages { private[payment] case class CreateOrUpdateKycDocument( creditedAccount: String, kycDocument: KycDocument - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PaymentAccountCommand { val key: String = creditedAccount } @@ -709,7 +747,8 @@ object PaymentMessages { case class LoadKycDocumentStatus( creditedAccount: String, kycDocumentType: KycDocument.KycDocumentType - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PaymentAccountCommand { val key: String = creditedAccount } @@ -723,7 +762,8 @@ object PaymentMessages { case class CreateOrUpdateUbo( creditedAccount: String, ubo: UboDeclaration.UltimateBeneficialOwner - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PaymentAccountCommand { val key: String = creditedAccount } @@ -731,14 +771,17 @@ object PaymentMessages { * - account which owns the UBO declaration that would be validated */ case class ValidateUboDeclaration(creditedAccount: String, ipAddress: String, userAgent: String) - extends PaymentCommandWithKey { + extends PaymentCommandWithKey + with PaymentAccountCommand { val key: String = creditedAccount } /** @param creditedAccount * - account which owns the UBO declaration that would be loaded */ - case class GetUboDeclaration(creditedAccount: String) extends PaymentCommandWithKey { + case class GetUboDeclaration(creditedAccount: String) + extends PaymentCommandWithKey + with PaymentAccountCommand { val key: String = creditedAccount } @@ -753,7 +796,8 @@ object PaymentMessages { private[payment] case class UpdateUboDeclarationStatus( uboDeclarationId: String, status: Option[UboDeclaration.UboDeclarationStatus] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PaymentAccountCommand { lazy val key: String = uboDeclarationId } @@ -765,7 +809,8 @@ object PaymentMessages { * - optional client id */ case class CreateMandate(creditedAccount: String, clientId: Option[String] = None) - extends PaymentCommandWithKey { + extends PaymentCommandWithKey + with PaymentAccountCommand { val key: String = creditedAccount } @@ -775,7 +820,8 @@ object PaymentMessages { * - optional client id */ case class CancelMandate(creditedAccount: String, clientId: Option[String] = None) - extends PaymentCommandWithKey { + extends PaymentCommandWithKey + with PaymentAccountCommand { val key: String = creditedAccount } @@ -790,7 +836,8 @@ object PaymentMessages { private[payment] case class UpdateMandateStatus( mandateId: String, status: Option[BankAccount.MandateStatus] = None - ) extends PaymentCommandWithKey { + ) extends PaymentCommandWithKey + with PaymentAccountCommand { lazy val key: String = mandateId } @@ -800,7 +847,9 @@ object PaymentMessages { * - user id */ @InternalApi - private[payment] case class ValidateRegularUser(userId: String) extends PaymentCommandWithKey { + private[payment] case class ValidateRegularUser(userId: String) + extends PaymentCommandWithKey + with PaymentAccountCommand { lazy val key: String = userId } @@ -810,7 +859,9 @@ object PaymentMessages { * - user id */ @InternalApi - private[payment] case class InvalidateRegularUser(userId: String) extends PaymentCommandWithKey { + private[payment] case class InvalidateRegularUser(userId: String) + extends PaymentCommandWithKey + with PaymentAccountCommand { lazy val key: String = userId } @@ -819,7 +870,8 @@ object PaymentMessages { */ @InternalApi private[payment] case class CreateOrUpdatePaymentAccount(paymentAccount: PaymentAccount) - extends PaymentCommandWithKey { + extends PaymentCommandWithKey + with PaymentAccountCommand { lazy val key: String = paymentAccount.externalUuidWithProfile } diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/typed/CardCommandHandler.scala b/core/src/main/scala/app/softnetwork/payment/persistence/typed/CardCommandHandler.scala new file mode 100644 index 0000000..c46c657 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/persistence/typed/CardCommandHandler.scala @@ -0,0 +1,694 @@ +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.concurrent.Completion +import app.softnetwork.payment.api.config.SoftPayClientSettings +import app.softnetwork.payment.message.PaymentEvents.{ + CardPreRegisteredEvent, + CardRegisteredEvent, + PaymentAccountUpsertedEvent, + WalletRegisteredEvent +} +import app.softnetwork.payment.message.PaymentMessages.{ + CancelPreAuthorization, + CardCommand, + CardDisabled, + CardNotDisabled, + CardNotPreAuthorized, + CardNotPreRegistered, + CardPreAuthorizationFailed, + CardPreAuthorized, + CardPreRegistered, + CardsLoaded, + CardsNotLoaded, + DisableCard, + LoadCards, + PayInWithCardPreAuthorized, + PayInWithCardPreAuthorizedFailed, + PaymentAccountNotFound, + PaymentRedirection, + PaymentResult, + PreAuthorizationCanceled, + PreAuthorizeCard, + PreAuthorizeCardCallback, + PreRegisterCard, + TransactionNotFound +} +import app.softnetwork.payment.message.TransactionEvents.{ + CardPreAuthorizationFailedEvent, + CardPreAuthorizedEvent, + PayInFailedEvent, + PreAuthorizationCanceledEvent +} +import app.softnetwork.payment.model.NaturalUser.NaturalUserType +import app.softnetwork.payment.model.{ + CardOwner, + PayInTransaction, + PaymentAccount, + PreAuthorizationTransaction, + Transaction +} +import app.softnetwork.persistence.now +import app.softnetwork.persistence.typed._ +import app.softnetwork.scheduler.message.SchedulerEvents.ExternalSchedulerEvent +import app.softnetwork.serialization.asJson +import app.softnetwork.time._ +import org.slf4j.Logger + +import scala.util.{Failure, Success} + +trait CardCommandHandler + extends EntityCommandHandler[CardCommand, PaymentAccount, ExternalSchedulerEvent, PaymentResult] + with PaymentCommandHandler + with PayInHandler + with Completion { + + override def apply( + entityId: String, + state: Option[PaymentAccount], + command: CardCommand, + replyTo: Option[ActorRef[PaymentResult]], + timers: TimerScheduler[CardCommand] + )(implicit + context: ActorContext[CardCommand] + ): 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: PreRegisterCard => + import cmd._ + var registerWallet: Boolean = false + 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.withNaturalUserType(NaturalUserType.PAYER) + ) + ), + acceptedTermsOfPSP = false, + None, + None + ) + case some => some + }) match { + case Some(userId) => + keyValueDao.addKeyValue(userId, entityId) + (paymentAccount.walletId match { + case None => + registerWallet = true + createOrUpdateWallet(Some(userId), currency, user.externalUuid, None) + case some => some + }) match { + case Some(walletId) => + keyValueDao.addKeyValue(walletId, entityId) + val paymentAccountUpsertedEvent = + PaymentAccountUpsertedEvent.defaultInstance + .withDocument( + paymentAccount + .withPaymentAccountStatus(PaymentAccount.PaymentAccountStatus.COMPTE_OK) + .copy(user = + PaymentAccount.User.NaturalUser( + user + .withUserId(userId) + .withWalletId(walletId) + .withNaturalUserType(NaturalUserType.PAYER) + ) + ) + .withLastUpdated(lastUpdated) + ) + .withLastUpdated(lastUpdated) + preRegisterCard(Some(userId), currency, user.externalUuid) match { + case Some(cardPreRegistration) => + keyValueDao.addKeyValue(cardPreRegistration.id, entityId) + val walletEvents: List[ExternalSchedulerEvent] = + if (registerWallet) { + List( + WalletRegisteredEvent.defaultInstance + .withOrderUuid(orderUuid) + .withExternalUuid(user.externalUuid) + .withUserId(userId) + .withWalletId(walletId) + .withLastUpdated(lastUpdated) + ) + } else { + List.empty + } + Effect + .persist( + List( + CardPreRegisteredEvent.defaultInstance + .withOrderUuid(orderUuid) + .withLastUpdated(lastUpdated) + .withExternalUuid(user.externalUuid) + .withUserId(userId) + .withWalletId(walletId) + .withCardPreRegistrationId(cardPreRegistration.id) + .withOwner( + CardOwner.defaultInstance + .withFirstName(user.firstName) + .withLastName(user.lastName) + .withBirthday(user.birthday) + ) + ) ++ walletEvents :+ paymentAccountUpsertedEvent + ) + .thenRun(_ => CardPreRegistered(cardPreRegistration) ~> replyTo) + case _ => + if (registerWallet) { + Effect + .persist( + List( + WalletRegisteredEvent.defaultInstance + .withOrderUuid(orderUuid) + .withExternalUuid(user.externalUuid) + .withUserId(userId) + .withWalletId(walletId) + .withLastUpdated(lastUpdated) + ) :+ paymentAccountUpsertedEvent + ) + .thenRun(_ => CardNotPreRegistered ~> replyTo) + } else { + Effect + .persist( + paymentAccountUpsertedEvent + ) + .thenRun(_ => CardNotPreRegistered ~> replyTo) + } + } + case _ => + Effect + .persist( + PaymentAccountUpsertedEvent.defaultInstance + .withDocument( + paymentAccount + .copy( + user = PaymentAccount.User.NaturalUser( + user.withUserId(userId).withNaturalUserType(NaturalUserType.PAYER) + ) + ) + .withLastUpdated(lastUpdated) + ) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => CardNotPreRegistered ~> replyTo) + } + case _ => + Effect + .persist( + PaymentAccountUpsertedEvent.defaultInstance + .withDocument( + paymentAccount + .withNaturalUser( + user.withNaturalUserType(NaturalUserType.PAYER) + ) + .withLastUpdated(lastUpdated) + ) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => CardNotPreRegistered ~> replyTo) + } + case _ => Effect.none.thenRun(_ => CardNotPreRegistered ~> replyTo) + } + + case cmd: PreAuthorizeCard => + 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 { + case Some(id) => + createCard(id, registrationData) + case _ => + paymentAccount.cards + .find(card => card.active.getOrElse(true) && !card.expired) + .map(_.id) + }) match { + case Some(cardId) => + val creditedUserId: Option[String] = + creditedAccount match { + case Some(account) => + paymentDao.loadPaymentAccount(account, clientId) complete () match { + case Success(s) => s.flatMap(_.userId) + case Failure(f) => + log.error(s"Error loading credited account: ${f.getMessage}") + None + } + case None => None + } + preAuthorizeCard( + PreAuthorizationTransaction.defaultInstance + .withCardId(cardId) + .withAuthorId(userId) + .withDebitedAmount(debitedAmount) + .withOrderUuid(orderUuid) + .withRegisterCard(registerCard) + .withPrintReceipt(printReceipt) + .copy( + ipAddress = ipAddress, + browserInfo = browserInfo, + creditedUserId = creditedUserId, + feesAmount = feesAmount + ) + ) match { + case Some(transaction) => + handleCardPreAuthorization( + entityId, + orderUuid, + replyTo, + paymentAccount, + registerCard, + printReceipt, + transaction + ) + case _ => // pre authorization failed + Effect.none.thenRun(_ => CardNotPreAuthorized ~> replyTo) + } + case _ => // no card id + Effect.none.thenRun(_ => CardNotPreAuthorized ~> replyTo) + } + case _ => // no userId + Effect.none.thenRun(_ => CardNotPreAuthorized ~> replyTo) + } + case _ => // no payment account + Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + case cmd: PreAuthorizeCardCallback => // 3DS + 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( + entityId, + orderUuid, + replyTo, + paymentAccount, + registerCard, + printReceipt, + transaction + ) + case _ => Effect.none.thenRun(_ => CardNotPreAuthorized ~> replyTo) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + case cmd: CancelPreAuthorization => + 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) + .copy(clientId = clientId) + ) + val lastUpdated = now() + Effect + .persist( + List( + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated), + PreAuthorizationCanceledEvent.defaultInstance + .withLastUpdated(lastUpdated) + .withOrderUuid(orderUuid) + .withDebitedAccount(paymentAccount.externalUuid) + .withCardPreAuthorizedTransactionId(cardPreAuthorizedTransactionId) + .withPreAuthorizationCanceled(preAuthorizationCanceled) + ) + ) + .thenRun(_ => PreAuthorizationCanceled(preAuthorizationCanceled) ~> replyTo) + case _ => // should never be the case + Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + case cmd: PayInWithCardPreAuthorized => + import cmd._ + state match { + case Some(paymentAccount) => + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) + val maybeTransaction = paymentAccount.transactions + .filter(t => t.`type` == Transaction.TransactionType.PRE_AUTHORIZATION) + .find(_.id == preAuthorizationId) + maybeTransaction match { + case None => + handlePayInWithCardPreauthorizedFailure( + "", + replyTo, + "PreAuthorizationTransactionNotFound" + ) + case Some(preAuthorizationTransaction) + if !Seq( + Transaction.TransactionStatus.TRANSACTION_CREATED, + Transaction.TransactionStatus.TRANSACTION_SUCCEEDED + ).contains( + preAuthorizationTransaction.status + ) => + handlePayInWithCardPreauthorizedFailure( + preAuthorizationTransaction.orderUuid, + replyTo, + "IllegalPreAuthorizationTransactionStatus" + ) + case Some(preAuthorizationTransaction) + if preAuthorizationTransaction.preAuthorizationCanceled.getOrElse(false) => + handlePayInWithCardPreauthorizedFailure( + preAuthorizationTransaction.orderUuid, + replyTo, + "PreAuthorizationCanceled" + ) + case Some(preAuthorizationTransaction) + if preAuthorizationTransaction.preAuthorizationValidated.getOrElse(false) => + handlePayInWithCardPreauthorizedFailure( + preAuthorizationTransaction.orderUuid, + replyTo, + "PreAuthorizationValidated" + ) + case Some(preAuthorizationTransaction) + if preAuthorizationTransaction.preAuthorizationExpired.getOrElse(false) => + handlePayInWithCardPreauthorizedFailure( + preAuthorizationTransaction.orderUuid, + replyTo, + "PreAuthorizationExpired" + ) + case Some(preAuthorizationTransaction) + if debitedAmount.getOrElse( + preAuthorizationTransaction.amount + ) > preAuthorizationTransaction.amount => + handlePayInWithCardPreauthorizedFailure( + preAuthorizationTransaction.orderUuid, + replyTo, + "DebitedAmountAbovePreAuthorizationAmount" + ) + case Some(preAuthorizationTransaction) => + // load credited payment account + 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) => + payIn( + Some( + PayInTransaction.defaultInstance + .withPaymentType(Transaction.PaymentType.PREAUTHORIZED) + .withCardPreAuthorizedTransactionId(preAuthorizationId) + .withAuthorId(preAuthorizationTransaction.authorId) + .withDebitedAmount( + debitedAmount.getOrElse(preAuthorizationTransaction.amount) + ) + .withCurrency(preAuthorizationTransaction.currency) + .withOrderUuid(preAuthorizationTransaction.orderUuid) + .withCreditedWalletId(creditedWalletId) + .withPreAuthorizationDebitedAmount( + preAuthorizationTransaction.amount + ) + ) + ) match { + case Some(transaction) => + handlePayIn( + entityId, + transaction.orderUuid, + replyTo, + paymentAccount, + registerCard = false, + printReceipt = false, + transaction + ) + case _ => + handlePayInWithCardPreauthorizedFailure( + preAuthorizationTransaction.orderUuid, + replyTo, + "TransactionNotSpecified" + ) + } + case _ => + handlePayInWithCardPreauthorizedFailure( + preAuthorizationTransaction.orderUuid, + replyTo, + "CreditedWalletNotFound" + ) + } + case _ => + handlePayInWithCardPreauthorizedFailure( + preAuthorizationTransaction.orderUuid, + replyTo, + "CreditedPaymentAccountNotFound" + ) + } + case Failure(_) => + handlePayInWithCardPreauthorizedFailure( + preAuthorizationTransaction.orderUuid, + replyTo, + "CreditedPaymentAccountNotFound" + ) + } + } + case _ => + handlePayInWithCardPreauthorizedFailure( + "", + replyTo, + "PaymentAccountNotFound" + ) + } + + case _: LoadCards => + state match { + case Some(paymentAccount) => + Effect.none.thenRun(_ => CardsLoaded(paymentAccount.cards) ~> replyTo) + case _ => Effect.none.thenRun(_ => CardsNotLoaded ~> replyTo) + } + + case cmd: DisableCard => + state match { + case Some(paymentAccount) => + paymentAccount.cards.find(_.id == cmd.cardId) match { + case Some(card) if card.getActive => + paymentAccount.recurryingPayments.find(r => + r.`type`.isCard && r.getCardId == cmd.cardId && + r.nextPaymentDate.isDefined + ) match { + 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() + Effect + .persist( + PaymentAccountUpsertedEvent.defaultInstance + .withDocument( + paymentAccount + .withCards( + paymentAccount.cards.filterNot(_.id == cmd.cardId) :+ card + .withActive(false) + ) + .withLastUpdated(lastUpdated) + ) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => CardDisabled ~> replyTo) + case _ => Effect.none.thenRun(_ => CardNotDisabled ~> replyTo) + } + } + case _ => Effect.none.thenRun(_ => CardNotDisabled ~> replyTo) + } + case _ => Effect.none.thenRun(_ => CardNotDisabled ~> replyTo) + } + + } + } + + private[this] def handleCardPreAuthorization( + entityId: String, + orderUuid: String, + replyTo: Option[ActorRef[PaymentResult]], + paymentAccount: PaymentAccount, + registerCard: Boolean, + printReceipt: Boolean, + transaction: Transaction + )(implicit + system: ActorSystem[_], + log: Logger, + softPayClientSettings: SoftPayClientSettings + ): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { + keyValueDao.addKeyValue( + transaction.id, + entityId + ) // add transaction id as a key for this payment account + val lastUpdated = now() + var updatedPaymentAccount = + paymentAccount + .withTransactions( + paymentAccount.transactions + .filterNot(_.id == transaction.id) :+ transaction + .copy(clientId = paymentAccount.clientId) + ) + .withLastUpdated(lastUpdated) + transaction.status match { + case Transaction.TransactionStatus.TRANSACTION_CREATED + if transaction.redirectUrl.isDefined => // 3ds + Effect + .persist( + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => + PaymentRedirection( + transaction.redirectUrl.get + ) ~> replyTo + ) + case _ => + if (transaction.status.isTransactionSucceeded || transaction.status.isTransactionCreated) { + log.debug( + "Order-{} pre authorized: {} -> {}", + 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 { + case Some(cardId) => + loadCard(cardId) match { + case Some(card) => + val updatedCard = updatedPaymentAccount.maybeUser match { + case Some(user) => + card + .withFirstName(user.firstName) + .withLastName(user.lastName) + .withBirthday(user.birthday) + case _ => card + } + updatedPaymentAccount = updatedPaymentAccount.withCards( + updatedPaymentAccount.cards.filterNot(_.id == updatedCard.id) :+ updatedCard + ) + List( + CardRegisteredEvent.defaultInstance + .withOrderUuid(orderUuid) + .withExternalUuid(paymentAccount.externalUuid) + .withCard(updatedCard) + .withLastUpdated(lastUpdated) + ) + case _ => List.empty + } + case _ => List.empty + } + } else { + List.empty + } + Effect + .persist( + registerCardEvents ++ + List( + CardPreAuthorizedEvent.defaultInstance + .withOrderUuid(orderUuid) + .withTransactionId(transaction.id) + .withCardId(transaction.getCardId) + .withDebitedAccount(paymentAccount.externalUuid) + .withDebitedAmount(transaction.amount) + .withLastUpdated(lastUpdated) + .withPrintReceipt(printReceipt) + ) :+ + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => CardPreAuthorized(transaction.id) ~> replyTo) + } else { + log.error( + "Order-{} could not be pre authorized: {} -> {}", + orderUuid, + transaction.id, + asJson(transaction) + ) + Effect + .persist( + List( + CardPreAuthorizationFailedEvent.defaultInstance + .withOrderUuid(orderUuid) + .withResultMessage(transaction.resultMessage) + .withTransaction(transaction) + ) :+ + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => CardPreAuthorizationFailed(transaction.resultMessage) ~> replyTo) + } + } + } + + private[this] def handlePayInWithCardPreauthorizedFailure( + orderUuid: String, + replyTo: Option[ActorRef[PaymentResult]], + reason: String + )(implicit context: ActorContext[_]): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { + + Effect + .persist( + List( + PayInFailedEvent.defaultInstance.withOrderUuid(orderUuid).withResultMessage(reason) + ) + ) + .thenRun(_ => PayInWithCardPreAuthorizedFailed(reason) ~> replyTo) + } + +} diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/typed/PayInCommandHandler.scala b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PayInCommandHandler.scala new file mode 100644 index 0000000..79eddf7 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PayInCommandHandler.scala @@ -0,0 +1,440 @@ +package app.softnetwork.payment.persistence.typed + +import akka.actor.typed.scaladsl.{ActorContext, TimerScheduler} +import akka.actor.typed.{ActorRef, ActorSystem} +import akka.persistence.typed.scaladsl.Effect +import app.softnetwork.concurrent.Completion +import app.softnetwork.payment.api.config.SoftPayClientSettings +import app.softnetwork.payment.config.PaymentSettings.PaymentConfig.payInStatementDescriptor +import app.softnetwork.payment.message.PaymentEvents.PaymentAccountUpsertedEvent +import app.softnetwork.payment.message.PaymentMessages.{ + LoadPayInTransaction, + PayIn, + PayInCallback, + PayInCommand, + PayInFailed, + PayInTransactionLoaded, + PaymentAccountNotFound, + PaymentResult, + TransactionNotFound +} +import app.softnetwork.payment.message.TransactionEvents.{PaidInEvent, PayInFailedEvent} +import app.softnetwork.payment.model.NaturalUser.NaturalUserType +import app.softnetwork.payment.model.{PayInTransaction, PaymentAccount, Transaction} +import app.softnetwork.persistence.now +import app.softnetwork.persistence.typed._ +import app.softnetwork.scheduler.message.SchedulerEvents.ExternalSchedulerEvent +import app.softnetwork.time._ +import org.slf4j.Logger + +import scala.util.{Failure, Success} + +trait PayInCommandHandler + extends EntityCommandHandler[ + PayInCommand, + PaymentAccount, + ExternalSchedulerEvent, + PaymentResult + ] + with PaymentCommandHandler + with PayInHandler + with Completion { + + override def apply( + entityId: String, + state: Option[PaymentAccount], + command: PayInCommand, + replyTo: Option[ActorRef[PaymentResult]], + timers: TimerScheduler[PayInCommand] + )(implicit + context: ActorContext[PayInCommand] + ): 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: PayIn => + import cmd._ + var registerWallet: Boolean = false + (state match { + case None => + cmd.user match { + case Some(user) => + 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.withNaturalUserType(NaturalUserType.PAYER) + ) + ), + acceptedTermsOfPSP = false, + None, + None + ) + case some => some + }) match { + case Some(userId) => + keyValueDao.addKeyValue(userId, entityId) + (paymentAccount.walletId match { + case None => + registerWallet = true + createOrUpdateWallet(Some(userId), currency, user.externalUuid, None) + case some => some + }) match { + case Some(walletId) => + keyValueDao.addKeyValue(walletId, entityId) + Some( + paymentAccount + .withPaymentAccountStatus( + PaymentAccount.PaymentAccountStatus.COMPTE_OK + ) + .copy(user = + PaymentAccount.User.NaturalUser( + user + .withUserId(userId) + .withWalletId(walletId) + .withNaturalUserType(NaturalUserType.PAYER) + ) + ) + .withLastUpdated(lastUpdated) + ) + case _ => + Some( + paymentAccount + .withPaymentAccountStatus( + PaymentAccount.PaymentAccountStatus.COMPTE_OK + ) + .copy(user = + PaymentAccount.User.NaturalUser( + user + .withUserId(userId) + .withNaturalUserType(NaturalUserType.PAYER) + ) + ) + .withLastUpdated(lastUpdated) + ) + } + } + } + case _ => + None + } + case some => some + }) match { + case Some(paymentAccount) => + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse(internalClientId) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ + paymentType match { + case Transaction.PaymentType.CARD => + paymentAccount.userId match { + case Some(userId) => + val cardId = + registrationId match { + case Some(id) => + createCard(id, registrationData) + case _ => + paymentAccount.cards + .find(card => card.active.getOrElse(true) && !card.expired) + .map(_.id) + } + // load credited payment account + paymentDao.loadPaymentAccount(creditedAccount, clientId) complete () match { + case Success(s) => + s match { + case Some(creditedPaymentAccount) => + creditedPaymentAccount.walletId match { + case Some(creditedWalletId) => + payIn( + Some( + PayInTransaction.defaultInstance + .withAuthorId(userId) + .withDebitedAmount(debitedAmount) + .withCurrency(currency) + .withOrderUuid(orderUuid) + .withCreditedWalletId(creditedWalletId) + .withCardId(cardId.orNull) + .withPaymentType(paymentType) + .withStatementDescriptor( + statementDescriptor.getOrElse(payInStatementDescriptor) + ) + .withRegisterCard(registerCard) + .withPrintReceipt(printReceipt) + .copy( + ipAddress = ipAddress, + browserInfo = browserInfo + ) + ) + ) match { + case Some(transaction) => + handlePayIn( + entityId, + orderUuid, + replyTo, + paymentAccount, + registerCard, + printReceipt, + transaction, + registerWallet + ) + case _ => + Effect.none.thenRun(_ => + PayInFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + "unknown" + ) ~> replyTo + ) + } + case _ => + Effect.none.thenRun(_ => + PayInFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + "no credited wallet" + ) ~> replyTo + ) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + case Failure(_) => + Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + case Transaction.PaymentType.PAYPAL => + paymentAccount.userId match { + case Some(userId) => + // load credited payment account + paymentDao.loadPaymentAccount(creditedAccount, clientId) complete () match { + case Success(s) => + s match { + case Some(creditedPaymentAccount) => + creditedPaymentAccount.walletId match { + case Some(creditedWalletId) => + payIn( + Some( + PayInTransaction.defaultInstance + .withPaymentType(Transaction.PaymentType.PAYPAL) + .withAuthorId(userId) + .withDebitedAmount(debitedAmount) + .withFeesAmount(feesAmount.getOrElse(0)) + .withCurrency(currency) + .withOrderUuid(orderUuid) + .withCreditedWalletId(creditedWalletId) + .withStatementDescriptor( + statementDescriptor.getOrElse(payInStatementDescriptor) + ) + .withPrintReceipt(printReceipt) + .copy( + ipAddress = ipAddress, + browserInfo = browserInfo + ) + ) + ) match { + case Some(transaction) => + handlePayIn( + entityId, + orderUuid, + replyTo, + paymentAccount, + registerCard = false, + printReceipt = printReceipt, + transaction, + registerWallet + ) + case _ => + Effect.none.thenRun(_ => + PayInFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + "unknown" + ) ~> replyTo + ) + } + case _ => + Effect.none.thenRun(_ => + PayInFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + "no credited wallet" + ) ~> replyTo + ) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + case Failure(_) => + Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + case _ => + Effect + .persist( + List( + PaidInEvent.defaultInstance + .withOrderUuid(orderUuid) + .withTransactionId("") + .withDebitedAccount(paymentAccount.externalUuid) + .withDebitedAmount(debitedAmount) + .withLastUpdated(now()) + .withCardId("") + .withPaymentType(paymentType) + ) + ) + .thenRun(_ => + PayInFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + s"$paymentType not supported" + ) ~> replyTo + ) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + case cmd: PayInCallback => + import cmd._ + state match { + case Some(paymentAccount) => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ + loadPayInTransaction(orderUuid, transactionId, None) match { + case Some(transaction) => + handlePayIn( + entityId, + orderUuid, + replyTo, + paymentAccount, + registerCard = registerCard, + printReceipt = printReceipt, + transaction + ) + case _ => Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + case cmd: LoadPayInTransaction => + import cmd._ + state match { + case Some(paymentAccount) => + paymentAccount.transactions.find(t => + t.id == transactionId && t.orderUuid == orderUuid + ) match { + case Some(transaction) + if transaction.status.isTransactionSucceeded + || transaction.status.isTransactionFailed + || transaction.status.isTransactionCanceled => + Effect.none.thenRun(_ => + PayInTransactionLoaded(transaction.id, transaction.status, None) ~> replyTo + ) + case Some(transaction) => + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ + loadPayInTransaction(orderUuid, transactionId, None) match { + case Some(t) => + val lastUpdated = now() + val updatedTransaction = transaction + .withStatus(t.status) + .withId(t.id) + .withAuthorId(t.authorId) + .withAmount(t.amount) + .withLastUpdated(lastUpdated) + .withPaymentType(t.paymentType) + .withResultCode(t.resultCode) + .withResultMessage(t.resultMessage) + .copy( + cardId = t.cardId.orElse(transaction.cardId) + ) + val updatedPaymentAccount = paymentAccount + .withTransactions( + paymentAccount.transactions.filterNot(_.id == t.id) + :+ updatedTransaction.copy(clientId = clientId) + ) + .withLastUpdated(lastUpdated) + if (t.status.isTransactionSucceeded || t.status.isTransactionCreated) { + Effect + .persist( + List( + PaidInEvent.defaultInstance + .withOrderUuid(orderUuid) + .withTransactionId(t.id) + .withDebitedAccount(t.authorId) + .withDebitedAmount(t.amount) + .withLastUpdated(lastUpdated) + .withCardId(t.cardId.orElse(transaction.cardId).getOrElse("")) + .withPaymentType(t.paymentType) + ) :+ + PaymentAccountUpsertedEvent.defaultInstance + .withLastUpdated(lastUpdated) + .withDocument(updatedPaymentAccount) + ) + .thenRun(_ => + PayInTransactionLoaded( + transaction.id, + transaction.status, + None + ) ~> replyTo + ) + } else { + Effect + .persist( + List( + PayInFailedEvent.defaultInstance + .withOrderUuid(orderUuid) + .withResultMessage(t.resultMessage) + .withTransaction(updatedTransaction) + ) :+ + PaymentAccountUpsertedEvent.defaultInstance + .withLastUpdated(lastUpdated) + .withDocument(updatedPaymentAccount) + ) + .thenRun(_ => + PayInTransactionLoaded( + transaction.id, + transaction.status, + None + ) ~> replyTo + ) + } + case None => Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) + } + case None => Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + } + } + +} diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/typed/PayInHandler.scala b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PayInHandler.scala new file mode 100644 index 0000000..8ddfd1e --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PayInHandler.scala @@ -0,0 +1,206 @@ +package app.softnetwork.payment.persistence.typed + +import akka.actor.typed.{ActorRef, ActorSystem} +import akka.persistence.typed.scaladsl.Effect +import app.softnetwork.payment.annotation.InternalApi +import app.softnetwork.payment.api.config.SoftPayClientSettings +import app.softnetwork.payment.message.PaymentEvents.{ + CardRegisteredEvent, + PaymentAccountUpsertedEvent, + WalletRegisteredEvent +} +import app.softnetwork.payment.message.PaymentMessages.{ + PaidIn, + PayInFailed, + PaymentRedirection, + PaymentRequired, + PaymentResult +} +import app.softnetwork.payment.message.TransactionEvents.{PaidInEvent, PayInFailedEvent} +import app.softnetwork.payment.model.{PaymentAccount, Transaction} +import app.softnetwork.persistence.now +import app.softnetwork.scheduler.message.SchedulerEvents.ExternalSchedulerEvent +import app.softnetwork.serialization.asJson +import app.softnetwork.persistence.typed._ +import app.softnetwork.time._ +import org.slf4j.Logger + +trait PayInHandler { _: PaymentCommandHandler => + + @InternalApi + private[payment] def handlePayIn( + entityId: String, + orderUuid: String, + replyTo: Option[ActorRef[PaymentResult]], + paymentAccount: PaymentAccount, + registerCard: Boolean, + printReceipt: Boolean, + transaction: Transaction, + registerWallet: Boolean = false + )(implicit + system: ActorSystem[_], + log: Logger, + softPayClientSettings: SoftPayClientSettings + ): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { + keyValueDao.addKeyValue( + transaction.id, + entityId + ) // add transaction id as a key for this payment account + val lastUpdated = now() + var updatedPaymentAccount = + paymentAccount + .withTransactions( + paymentAccount.transactions + .filterNot(_.id == transaction.id) :+ transaction + ) + .withLastUpdated(lastUpdated) + val walletEvents: List[ExternalSchedulerEvent] = + if (registerWallet) { + List( + WalletRegisteredEvent.defaultInstance + .withOrderUuid(orderUuid) + .withExternalUuid(paymentAccount.externalUuid) + .withLastUpdated(lastUpdated) + .copy( + userId = paymentAccount.userId.get, + walletId = paymentAccount.walletId.get + ) + ) + } else { + List.empty + } + transaction.status match { + case Transaction.TransactionStatus.TRANSACTION_PENDING_PAYMENT + if transaction.paymentClientReturnUrl.isDefined => + Effect + .persist( + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) +: walletEvents + ) + .thenRun(_ => + PaymentRequired( + transaction.id, + transaction.paymentClientSecret.getOrElse(""), + transaction.paymentClientData.getOrElse(""), + transaction.paymentClientReturnUrl.get + ) ~> replyTo + ) + case Transaction.TransactionStatus.TRANSACTION_CREATED + if transaction.redirectUrl.isDefined => // 3ds | PayPal + Effect + .persist( + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) +: walletEvents + ) + .thenRun(_ => PaymentRedirection(transaction.redirectUrl.get) ~> replyTo) + 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 { + case Some(cardId) => + loadCard(cardId) match { + case Some(card) => + val updatedCard = updatedPaymentAccount.maybeUser match { + case Some(user) => + card + .withFirstName(user.firstName) + .withLastName(user.lastName) + .withBirthday(user.birthday) + case _ => card + } + updatedPaymentAccount = updatedPaymentAccount.withCards( + updatedPaymentAccount.cards.filterNot(_.id == updatedCard.id) :+ updatedCard + ) + List( + CardRegisteredEvent.defaultInstance + .withOrderUuid(orderUuid) + .withExternalUuid(paymentAccount.externalUuid) + .withCard(updatedCard) + .withLastUpdated(lastUpdated) + ) + case _ => List.empty + } + case _ => List.empty + } + } else { + List.empty + } + updatedPaymentAccount = transaction.preAuthorizationId match { + case Some(preAuthorizationId) => + transaction.preAuthorizationDebitedAmount match { + case Some(preAuthorizationDebitedAmount) + if transaction.amount < preAuthorizationDebitedAmount => + // validation required + val updatedTransaction = updatedPaymentAccount.transactions + .find(_.id == preAuthorizationId) + .map( + _.copy( + preAuthorizationValidated = + Some(validatePreAuthorization(transaction.orderUuid, preAuthorizationId)), + clientId = clientId + ) + ) + updatedPaymentAccount.withTransactions( + updatedPaymentAccount.transactions.filterNot(_.id == preAuthorizationId) ++ Seq( + updatedTransaction + ).flatten + ) + case _ => updatedPaymentAccount + } + case _ => updatedPaymentAccount + } + Effect + .persist( + registerCardEvents ++ + List( + PaidInEvent.defaultInstance + .withOrderUuid(orderUuid) + .withTransactionId(transaction.id) + .withDebitedAccount(paymentAccount.externalUuid) + .withDebitedAmount(transaction.amount) + .withLastUpdated(lastUpdated) + .withCardId(transaction.cardId.getOrElse("")) + .withPaymentType(transaction.paymentType) + .withPrintReceipt(printReceipt) + ) ++ + (PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) +: walletEvents) + ) + .thenRun(_ => PaidIn(transaction.id, transaction.status) ~> replyTo) + } else { + log.error( + "Order-{} could not be paid in: {} -> {}", + orderUuid, + transaction.id, + asJson(transaction) + ) + Effect + .persist( + List( + PayInFailedEvent.defaultInstance + .withOrderUuid(orderUuid) + .withResultMessage(transaction.resultMessage) + .withTransaction(transaction) + ) ++ + (PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) +: walletEvents) + ) + .thenRun(_ => + PayInFailed(transaction.id, transaction.status, transaction.resultMessage) ~> replyTo + ) + } + } + } + +} diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/typed/PayOutCommandHandler.scala b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PayOutCommandHandler.scala new file mode 100644 index 0000000..9734bb6 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PayOutCommandHandler.scala @@ -0,0 +1,353 @@ +package app.softnetwork.payment.persistence.typed + +import akka.actor.typed.scaladsl.{ActorContext, TimerScheduler} +import akka.actor.typed.{ActorRef, ActorSystem} +import akka.persistence.typed.scaladsl.Effect +import app.softnetwork.concurrent.Completion +import app.softnetwork.payment.api.config.SoftPayClientSettings +import app.softnetwork.payment.message.PaymentEvents.PaymentAccountUpsertedEvent +import app.softnetwork.payment.message.PaymentMessages._ +import app.softnetwork.payment.message.TransactionEvents.{PaidOutEvent, PayOutFailedEvent} +import app.softnetwork.payment.model.{PayOutTransaction, PaymentAccount, Transaction} +import app.softnetwork.persistence.now +import app.softnetwork.persistence.typed._ +import app.softnetwork.scheduler.message.SchedulerEvents.ExternalSchedulerEvent +import app.softnetwork.serialization.asJson +import app.softnetwork.time._ +import org.slf4j.Logger + +trait PayOutCommandHandler + extends EntityCommandHandler[ + PayOutCommand, + PaymentAccount, + ExternalSchedulerEvent, + PaymentResult + ] + with PaymentCommandHandler + with Completion { + + override def apply( + entityId: String, + state: Option[PaymentAccount], + command: PayOutCommand, + replyTo: Option[ActorRef[PaymentResult]], + timers: TimerScheduler[PayOutCommand] + )(implicit + context: ActorContext[PayOutCommand] + ): 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: PayOut => + 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 { + case Some(walletId) => + paymentAccount.bankAccount.flatMap(_.id) match { + case Some(bankAccountId) => + val pit = payInTransactionId.orElse( + paymentAccount.transactions + .filter(t => + t.`type` == Transaction.TransactionType.PAYIN && + (t.status.isTransactionCreated || t.status.isTransactionSucceeded) + ) + .find(_.orderUuid == orderUuid) + .map(_.id) + ) + payOut( + Some( + PayOutTransaction.defaultInstance + .withBankAccountId(bankAccountId) + .withDebitedAmount(creditedAmount) + .withOrderUuid(orderUuid) + .withFeesAmount(feesAmount) + .withCurrency(currency) + .withAuthorId(userId) + .withCreditedUserId(userId) + .withDebitedWalletId(walletId) + .copy( + externalReference = externalReference, + payInTransactionId = pit + ) + ) + ) match { + case Some(transaction) => + keyValueDao.addKeyValue(transaction.id, entityId) + val lastUpdated = now() + val updatedPaymentAccount = paymentAccount + .withTransactions( + paymentAccount.transactions.filterNot(_.id == transaction.id) + :+ transaction.copy(clientId = clientId) + ) + .withLastUpdated(lastUpdated) + if (transaction.status.isTransactionFailedForTechnicalReason) { + log.error( + "Order-{} could not be paid out: {} -> {}", + orderUuid, + transaction.id, + asJson(transaction) + ) + Effect + .persist( + List( + PayOutFailedEvent.defaultInstance + .withOrderUuid(orderUuid) + .withResultMessage(transaction.resultMessage) + .withTransaction(transaction) + .copy(externalReference = externalReference) + ) :+ + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => + PayOutFailed( + transaction.id, + transaction.status, + transaction.resultMessage + ) ~> replyTo + ) + } else if ( + transaction.status.isTransactionSucceeded || transaction.status.isTransactionCreated + ) { + log.info( + "Order-{} paid out : {} -> {}", + orderUuid, + transaction.id, + asJson(transaction) + ) + Effect + .persist( + List( + PaidOutEvent.defaultInstance + .withOrderUuid(orderUuid) + .withLastUpdated(lastUpdated) + .withCreditedAccount(paymentAccount.externalUuid) + .withCreditedAmount(creditedAmount) + .withFeesAmount(feesAmount) + .withCurrency(currency) + .withTransactionId(transaction.id) + .withPaymentType(transaction.paymentType) + .copy(externalReference = externalReference) + ) :+ + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => + PaidOut(transaction.id, transaction.status) ~> replyTo + ) + } else { + log.error( + "Order-{} could not be paid out : {} -> {}", + orderUuid, + transaction.id, + asJson(transaction) + ) + Effect + .persist( + List( + PayOutFailedEvent.defaultInstance + .withOrderUuid(orderUuid) + .withResultMessage(transaction.resultMessage) + .withTransaction(transaction) + .copy(externalReference = externalReference) + ) :+ + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => + PayOutFailed( + transaction.id, + transaction.status, + transaction.resultMessage + ) ~> replyTo + ) + } + case _ => + log.error( + "Order-{} could not be paid out: no transaction returned by provider", + orderUuid + ) + Effect + .persist( + List( + PayOutFailedEvent.defaultInstance + .withOrderUuid(orderUuid) + .withResultMessage("no transaction returned by provider") + .copy(externalReference = externalReference) + ) + ) + .thenRun(_ => + PayOutFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + "no transaction returned by provider" + ) ~> replyTo + ) + } + case _ => + Effect + .persist( + List( + PayOutFailedEvent.defaultInstance + .withOrderUuid(orderUuid) + .withResultMessage("no bank account") + .copy(externalReference = externalReference) + ) + ) + .thenRun(_ => + PayOutFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + "no bank account" + ) ~> replyTo + ) + } + case _ => + Effect + .persist( + List( + PayOutFailedEvent.defaultInstance + .withOrderUuid(orderUuid) + .withResultMessage("no wallet id") + .copy(externalReference = externalReference) + ) + ) + .thenRun(_ => + PayOutFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + "no wallet id" + ) ~> replyTo + ) + } + case _ => + Effect + .persist( + List( + PayOutFailedEvent.defaultInstance + .withOrderUuid(orderUuid) + .withResultMessage("no payment provider user id") + .copy(externalReference = externalReference) + ) + ) + .thenRun(_ => + PayOutFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + "no payment provider user id" + ) ~> replyTo + ) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + case cmd: LoadPayOutTransaction => + import cmd._ + state match { + case Some(paymentAccount) => + paymentAccount.transactions.find(t => + t.id == transactionId && t.orderUuid == orderUuid + ) match { + case Some(transaction) + if transaction.status.isTransactionSucceeded + || transaction.status.isTransactionFailed + || transaction.status.isTransactionCanceled => + Effect.none.thenRun(_ => + PayOutTransactionLoaded(transaction.id, transaction.status, None) ~> replyTo + ) + case Some(transaction) => + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ + loadPayOutTransaction(orderUuid, transactionId) match { + case Some(t) => + val lastUpdated = now() + val updatedTransaction = transaction + .withStatus(t.status) + .withLastUpdated(lastUpdated) + .withResultCode(t.resultCode) + .withResultMessage(t.resultMessage) + val updatedPaymentAccount = paymentAccount + .withTransactions( + paymentAccount.transactions.filterNot(_.id == t.id) + :+ updatedTransaction.copy(clientId = clientId) + ) + .withLastUpdated(lastUpdated) + if (t.status.isTransactionSucceeded || t.status.isTransactionCreated) { + Effect + .persist( + List( + PaidOutEvent.defaultInstance + .withOrderUuid(orderUuid) + .withLastUpdated(lastUpdated) + .withCreditedAccount(paymentAccount.externalUuid) + .withCreditedAmount(t.amount) + .withFeesAmount(t.fees) + .withCurrency(t.currency) + .withTransactionId(t.id) + .withPaymentType(t.paymentType) + .copy(externalReference = transaction.externalReference) + ) :+ + PaymentAccountUpsertedEvent.defaultInstance + .withLastUpdated(lastUpdated) + .withDocument(updatedPaymentAccount) + ) + .thenRun(_ => + PayOutTransactionLoaded( + transaction.id, + transaction.status, + None + ) ~> replyTo + ) + } else { + Effect + .persist( + List( + PayOutFailedEvent.defaultInstance + .withOrderUuid(orderUuid) + .withResultMessage(updatedTransaction.resultMessage) + .withTransaction(updatedTransaction) + .copy(externalReference = transaction.externalReference) + ) :+ + PaymentAccountUpsertedEvent.defaultInstance + .withLastUpdated(lastUpdated) + .withDocument(updatedPaymentAccount) + ) + .thenRun(_ => + PayOutTransactionLoaded( + transaction.id, + transaction.status, + None + ) ~> replyTo + ) + } + case None => Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) + } + case _ => Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + } + } + +} diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala index d421735..8d10236 100644 --- a/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala +++ b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala @@ -49,15 +49,12 @@ trait PaymentBehavior ExternalSchedulerEvent, PaymentResult ] - with ManifestWrapper[PaymentAccount] { + with ManifestWrapper[PaymentAccount] + with PaymentCommandHandler + with PaymentTimers { self => override protected val manifestWrapper: ManifestW = ManifestW() - lazy val keyValueDao: GenericKeyValueDao = - PaymentKvDao //FIXME app.softnetwork.payment.persistence.data.paymentKvDao - - val nextRecurringPayment: String = "NextRecurringPayment" - def paymentDao: PaymentDao = PaymentDao def softPayAccountDao: SoftPayAccountDao = SoftPayAccountDao @@ -92,6 +89,85 @@ trait PaymentBehavior case _ => Set(persistenceId) } + private[this] lazy val cardCommandHandler: PartialFunction[PaymentCommand, EntityCommandHandler[ + PaymentCommand, + PaymentAccount, + ExternalSchedulerEvent, + PaymentResult + ]] = { case _: CardCommand => + new CardCommandHandler { + override def paymentDao: PaymentDao = self.paymentDao + override def softPayAccountDao: SoftPayAccountDao = self.softPayAccountDao + }.asInstanceOf[EntityCommandHandler[ + PaymentCommand, + PaymentAccount, + ExternalSchedulerEvent, + PaymentResult + ]] + } + + private[this] lazy val payInCommandHandler: PartialFunction[PaymentCommand, EntityCommandHandler[ + PaymentCommand, + PaymentAccount, + ExternalSchedulerEvent, + PaymentResult + ]] = { case _: PayInCommand => + new PayInCommandHandler { + override def paymentDao: PaymentDao = self.paymentDao + override def softPayAccountDao: SoftPayAccountDao = self.softPayAccountDao + }.asInstanceOf[EntityCommandHandler[ + PaymentCommand, + PaymentAccount, + ExternalSchedulerEvent, + PaymentResult + ]] + } + + private[this] lazy val payOutCommandHandler: PartialFunction[PaymentCommand, EntityCommandHandler[ + PaymentCommand, + PaymentAccount, + ExternalSchedulerEvent, + PaymentResult + ]] = { case _: PayOutCommand => + new PayOutCommandHandler { + override def paymentDao: PaymentDao = self.paymentDao + override def softPayAccountDao: SoftPayAccountDao = self.softPayAccountDao + }.asInstanceOf[EntityCommandHandler[ + PaymentCommand, + PaymentAccount, + ExternalSchedulerEvent, + PaymentResult + ]] + } + + private[this] lazy val recurringPaymentCommandHandler + : PartialFunction[PaymentCommand, EntityCommandHandler[ + PaymentCommand, + PaymentAccount, + ExternalSchedulerEvent, + PaymentResult + ]] = { case _: RecurringPaymentCommand => + new RecurringPaymentCommandHandler { + override def paymentDao: PaymentDao = self.paymentDao + override def softPayAccountDao: SoftPayAccountDao = self.softPayAccountDao + override def persistenceId: String = self.persistenceId + }.asInstanceOf[EntityCommandHandler[ + PaymentCommand, + PaymentAccount, + ExternalSchedulerEvent, + PaymentResult + ]] + } + + override def entityCommandHandler: PartialFunction[PaymentCommand, EntityCommandHandler[ + PaymentCommand, + PaymentAccount, + ExternalSchedulerEvent, + PaymentResult + ]] = { + cardCommandHandler orElse payInCommandHandler orElse payOutCommandHandler orElse recurringPaymentCommandHandler orElse super.entityCommandHandler + } + /** @param entityId * - entity identity * @param state @@ -170,739 +246,34 @@ trait PaymentBehavior } } updatedPaymentAccount.transactions.foreach { transaction => - keyValueDao.addKeyValue(transaction.id, entityId) - } - if (updatedPaymentAccount.legalUser) { - updatedPaymentAccount.getLegalUser.uboDeclaration match { - case Some(declaration) => keyValueDao.addKeyValue(declaration.id, entityId) - case _ => - } - } - Effect - .persist( - broadcastEvent( - PaymentAccountCreatedOrUpdatedEvent.defaultInstance - .withLastUpdated(lastUpdated) - .withExternalUuid(updatedPaymentAccount.externalUuid) - .copy(profile = updatedPaymentAccount.profile) - ) :+ - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => { - val result = - if (updated) - PaymentAccountUpdated - else - PaymentAccountCreated - result ~> replyTo - }) - - case cmd: PreRegisterCard => - import cmd._ - var registerWallet: Boolean = false - 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.withNaturalUserType(NaturalUserType.PAYER) - ) - ), - acceptedTermsOfPSP = false, - None, - None - ) - case some => some - }) match { - case Some(userId) => - keyValueDao.addKeyValue(userId, entityId) - (paymentAccount.walletId match { - case None => - registerWallet = true - createOrUpdateWallet(Some(userId), currency, user.externalUuid, None) - case some => some - }) match { - case Some(walletId) => - keyValueDao.addKeyValue(walletId, entityId) - val paymentAccountUpsertedEvent = - PaymentAccountUpsertedEvent.defaultInstance - .withDocument( - paymentAccount - .withPaymentAccountStatus(PaymentAccount.PaymentAccountStatus.COMPTE_OK) - .copy(user = - PaymentAccount.User.NaturalUser( - user - .withUserId(userId) - .withWalletId(walletId) - .withNaturalUserType(NaturalUserType.PAYER) - ) - ) - .withLastUpdated(lastUpdated) - ) - .withLastUpdated(lastUpdated) - preRegisterCard(Some(userId), currency, user.externalUuid) match { - case Some(cardPreRegistration) => - keyValueDao.addKeyValue(cardPreRegistration.id, entityId) - val walletEvents: List[ExternalSchedulerEvent] = - if (registerWallet) { - broadcastEvent( - WalletRegisteredEvent.defaultInstance - .withOrderUuid(orderUuid) - .withExternalUuid(user.externalUuid) - .withUserId(userId) - .withWalletId(walletId) - .withLastUpdated(lastUpdated) - ) - } else { - List.empty - } - Effect - .persist( - broadcastEvent( - CardPreRegisteredEvent.defaultInstance - .withOrderUuid(orderUuid) - .withLastUpdated(lastUpdated) - .withExternalUuid(user.externalUuid) - .withUserId(userId) - .withWalletId(walletId) - .withCardPreRegistrationId(cardPreRegistration.id) - .withOwner( - CardOwner.defaultInstance - .withFirstName(user.firstName) - .withLastName(user.lastName) - .withBirthday(user.birthday) - ) - ) ++ walletEvents :+ paymentAccountUpsertedEvent - ) - .thenRun(_ => CardPreRegistered(cardPreRegistration) ~> replyTo) - case _ => - if (registerWallet) { - Effect - .persist( - broadcastEvent( - WalletRegisteredEvent.defaultInstance - .withOrderUuid(orderUuid) - .withExternalUuid(user.externalUuid) - .withUserId(userId) - .withWalletId(walletId) - .withLastUpdated(lastUpdated) - ) :+ paymentAccountUpsertedEvent - ) - .thenRun(_ => CardNotPreRegistered ~> replyTo) - } else { - Effect - .persist( - paymentAccountUpsertedEvent - ) - .thenRun(_ => CardNotPreRegistered ~> replyTo) - } - } - case _ => - Effect - .persist( - PaymentAccountUpsertedEvent.defaultInstance - .withDocument( - paymentAccount - .copy( - user = PaymentAccount.User.NaturalUser( - user.withUserId(userId).withNaturalUserType(NaturalUserType.PAYER) - ) - ) - .withLastUpdated(lastUpdated) - ) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => CardNotPreRegistered ~> replyTo) - } - case _ => - Effect - .persist( - PaymentAccountUpsertedEvent.defaultInstance - .withDocument( - paymentAccount - .withNaturalUser( - user.withNaturalUserType(NaturalUserType.PAYER) - ) - .withLastUpdated(lastUpdated) - ) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => CardNotPreRegistered ~> replyTo) - } - case _ => Effect.none.thenRun(_ => CardNotPreRegistered ~> replyTo) - } - - case cmd: PreAuthorizeCard => - 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 { - case Some(id) => - createCard(id, registrationData) - case _ => - paymentAccount.cards - .find(card => card.active.getOrElse(true) && !card.expired) - .map(_.id) - }) match { - case Some(cardId) => - val creditedUserId: Option[String] = - creditedAccount match { - case Some(account) => - paymentDao.loadPaymentAccount(account, clientId) complete () match { - case Success(s) => s.flatMap(_.userId) - case Failure(f) => - log.error(s"Error loading credited account: ${f.getMessage}") - None - } - case None => None - } - preAuthorizeCard( - PreAuthorizationTransaction.defaultInstance - .withCardId(cardId) - .withAuthorId(userId) - .withDebitedAmount(debitedAmount) - .withOrderUuid(orderUuid) - .withRegisterCard(registerCard) - .withPrintReceipt(printReceipt) - .copy( - ipAddress = ipAddress, - browserInfo = browserInfo, - creditedUserId = creditedUserId, - feesAmount = feesAmount - ) - ) match { - case Some(transaction) => - handleCardPreAuthorization( - entityId, - orderUuid, - replyTo, - paymentAccount, - registerCard, - printReceipt, - transaction - ) - case _ => // pre authorization failed - Effect.none.thenRun(_ => CardNotPreAuthorized ~> replyTo) - } - case _ => // no card id - Effect.none.thenRun(_ => CardNotPreAuthorized ~> replyTo) - } - case _ => // no userId - Effect.none.thenRun(_ => CardNotPreAuthorized ~> replyTo) - } - case _ => // no payment account - Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - - case cmd: PreAuthorizeCardCallback => // 3DS - 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( - entityId, - orderUuid, - replyTo, - paymentAccount, - registerCard, - printReceipt, - transaction - ) - case _ => Effect.none.thenRun(_ => CardNotPreAuthorized ~> replyTo) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - - case cmd: CancelPreAuthorization => - 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) - .copy(clientId = clientId) - ) - val lastUpdated = now() - Effect - .persist( - List( - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated), - PreAuthorizationCanceledEvent.defaultInstance - .withLastUpdated(lastUpdated) - .withOrderUuid(orderUuid) - .withDebitedAccount(paymentAccount.externalUuid) - .withCardPreAuthorizedTransactionId(cardPreAuthorizedTransactionId) - .withPreAuthorizationCanceled(preAuthorizationCanceled) - ) - ) - .thenRun(_ => PreAuthorizationCanceled(preAuthorizationCanceled) ~> replyTo) - case _ => // should never be the case - Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - - case cmd: PayInWithCardPreAuthorized => - import cmd._ - state match { - case Some(paymentAccount) => - val clientId = paymentAccount.clientId - .orElse(cmd.clientId) - .orElse( - internalClientId - ) - val maybeTransaction = paymentAccount.transactions - .filter(t => t.`type` == Transaction.TransactionType.PRE_AUTHORIZATION) - .find(_.id == preAuthorizationId) - maybeTransaction match { - case None => - handlePayInWithCardPreauthorizedFailure( - "", - replyTo, - "PreAuthorizationTransactionNotFound" - ) - case Some(preAuthorizationTransaction) - if !Seq( - Transaction.TransactionStatus.TRANSACTION_CREATED, - Transaction.TransactionStatus.TRANSACTION_SUCCEEDED - ).contains( - preAuthorizationTransaction.status - ) => - handlePayInWithCardPreauthorizedFailure( - preAuthorizationTransaction.orderUuid, - replyTo, - "IllegalPreAuthorizationTransactionStatus" - ) - case Some(preAuthorizationTransaction) - if preAuthorizationTransaction.preAuthorizationCanceled.getOrElse(false) => - handlePayInWithCardPreauthorizedFailure( - preAuthorizationTransaction.orderUuid, - replyTo, - "PreAuthorizationCanceled" - ) - case Some(preAuthorizationTransaction) - if preAuthorizationTransaction.preAuthorizationValidated.getOrElse(false) => - handlePayInWithCardPreauthorizedFailure( - preAuthorizationTransaction.orderUuid, - replyTo, - "PreAuthorizationValidated" - ) - case Some(preAuthorizationTransaction) - if preAuthorizationTransaction.preAuthorizationExpired.getOrElse(false) => - handlePayInWithCardPreauthorizedFailure( - preAuthorizationTransaction.orderUuid, - replyTo, - "PreAuthorizationExpired" - ) - case Some(preAuthorizationTransaction) - if debitedAmount.getOrElse( - preAuthorizationTransaction.amount - ) > preAuthorizationTransaction.amount => - handlePayInWithCardPreauthorizedFailure( - preAuthorizationTransaction.orderUuid, - replyTo, - "DebitedAmountAbovePreAuthorizationAmount" - ) - case Some(preAuthorizationTransaction) => - // load credited payment account - 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) => - payIn( - Some( - PayInTransaction.defaultInstance - .withPaymentType(Transaction.PaymentType.PREAUTHORIZED) - .withCardPreAuthorizedTransactionId(preAuthorizationId) - .withAuthorId(preAuthorizationTransaction.authorId) - .withDebitedAmount( - debitedAmount.getOrElse(preAuthorizationTransaction.amount) - ) - .withCurrency(preAuthorizationTransaction.currency) - .withOrderUuid(preAuthorizationTransaction.orderUuid) - .withCreditedWalletId(creditedWalletId) - .withPreAuthorizationDebitedAmount( - preAuthorizationTransaction.amount - ) - ) - ) match { - case Some(transaction) => - handlePayIn( - entityId, - transaction.orderUuid, - replyTo, - paymentAccount, - registerCard = false, - printReceipt = false, - transaction - ) - case _ => - handlePayInWithCardPreauthorizedFailure( - preAuthorizationTransaction.orderUuid, - replyTo, - "TransactionNotSpecified" - ) - } - case _ => - handlePayInWithCardPreauthorizedFailure( - preAuthorizationTransaction.orderUuid, - replyTo, - "CreditedWalletNotFound" - ) - } - case _ => - handlePayInWithCardPreauthorizedFailure( - preAuthorizationTransaction.orderUuid, - replyTo, - "CreditedPaymentAccountNotFound" - ) - } - case Failure(_) => - handlePayInWithCardPreauthorizedFailure( - preAuthorizationTransaction.orderUuid, - replyTo, - "CreditedPaymentAccountNotFound" - ) - } - } - case _ => - handlePayInWithCardPreauthorizedFailure( - "", - replyTo, - "PaymentAccountNotFound" - ) - } - - case cmd: PayIn => - import cmd._ - var registerWallet: Boolean = false - (state match { - case None => - cmd.user match { - case Some(user) => - 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.withNaturalUserType(NaturalUserType.PAYER) - ) - ), - acceptedTermsOfPSP = false, - None, - None - ) - case some => some - }) match { - case Some(userId) => - keyValueDao.addKeyValue(userId, entityId) - (paymentAccount.walletId match { - case None => - registerWallet = true - createOrUpdateWallet(Some(userId), currency, user.externalUuid, None) - case some => some - }) match { - case Some(walletId) => - keyValueDao.addKeyValue(walletId, entityId) - Some( - paymentAccount - .withPaymentAccountStatus( - PaymentAccount.PaymentAccountStatus.COMPTE_OK - ) - .copy(user = - PaymentAccount.User.NaturalUser( - user - .withUserId(userId) - .withWalletId(walletId) - .withNaturalUserType(NaturalUserType.PAYER) - ) - ) - .withLastUpdated(lastUpdated) - ) - case _ => - Some( - paymentAccount - .withPaymentAccountStatus( - PaymentAccount.PaymentAccountStatus.COMPTE_OK - ) - .copy(user = - PaymentAccount.User.NaturalUser( - user - .withUserId(userId) - .withNaturalUserType(NaturalUserType.PAYER) - ) - ) - .withLastUpdated(lastUpdated) - ) - } - } - } - case _ => - None - } - case some => some - }) match { - case Some(paymentAccount) => - val clientId = paymentAccount.clientId - .orElse(cmd.clientId) - .orElse(internalClientId) - val paymentProvider = loadPaymentProvider(clientId) - import paymentProvider._ - paymentType match { - case Transaction.PaymentType.CARD => - paymentAccount.userId match { - case Some(userId) => - val cardId = - registrationId match { - case Some(id) => - createCard(id, registrationData) - case _ => - paymentAccount.cards - .find(card => card.active.getOrElse(true) && !card.expired) - .map(_.id) - } - // load credited payment account - paymentDao.loadPaymentAccount(creditedAccount, clientId) complete () match { - case Success(s) => - s match { - case Some(creditedPaymentAccount) => - creditedPaymentAccount.walletId match { - case Some(creditedWalletId) => - payIn( - Some( - PayInTransaction.defaultInstance - .withAuthorId(userId) - .withDebitedAmount(debitedAmount) - .withCurrency(currency) - .withOrderUuid(orderUuid) - .withCreditedWalletId(creditedWalletId) - .withCardId(cardId.orNull) - .withPaymentType(paymentType) - .withStatementDescriptor( - statementDescriptor.getOrElse(payInStatementDescriptor) - ) - .withRegisterCard(registerCard) - .withPrintReceipt(printReceipt) - .copy( - ipAddress = ipAddress, - browserInfo = browserInfo - ) - ) - ) match { - case Some(transaction) => - handlePayIn( - entityId, - orderUuid, - replyTo, - paymentAccount, - registerCard, - printReceipt, - transaction, - registerWallet - ) - case _ => - Effect.none.thenRun(_ => - PayInFailed( - "", - Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, - "unknown" - ) ~> replyTo - ) - } - case _ => - Effect.none.thenRun(_ => - PayInFailed( - "", - Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, - "no credited wallet" - ) ~> replyTo - ) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - case Failure(_) => - Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - - case Transaction.PaymentType.PAYPAL => - paymentAccount.userId match { - case Some(userId) => - // load credited payment account - paymentDao.loadPaymentAccount(creditedAccount, clientId) complete () match { - case Success(s) => - s match { - case Some(creditedPaymentAccount) => - creditedPaymentAccount.walletId match { - case Some(creditedWalletId) => - payIn( - Some( - PayInTransaction.defaultInstance - .withPaymentType(Transaction.PaymentType.PAYPAL) - .withAuthorId(userId) - .withDebitedAmount(debitedAmount) - .withFeesAmount(feesAmount.getOrElse(0)) - .withCurrency(currency) - .withOrderUuid(orderUuid) - .withCreditedWalletId(creditedWalletId) - .withStatementDescriptor( - statementDescriptor.getOrElse(payInStatementDescriptor) - ) - .withPrintReceipt(printReceipt) - .copy( - ipAddress = ipAddress, - browserInfo = browserInfo - ) - ) - ) match { - case Some(transaction) => - handlePayIn( - entityId, - orderUuid, - replyTo, - paymentAccount, - registerCard = false, - printReceipt = printReceipt, - transaction, - registerWallet - ) - case _ => - Effect.none.thenRun(_ => - PayInFailed( - "", - Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, - "unknown" - ) ~> replyTo - ) - } - case _ => - Effect.none.thenRun(_ => - PayInFailed( - "", - Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, - "no credited wallet" - ) ~> replyTo - ) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - case Failure(_) => - Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - - case _ => - Effect - .persist( - broadcastEvent( - PaidInEvent.defaultInstance - .withOrderUuid(orderUuid) - .withTransactionId("") - .withDebitedAccount(paymentAccount.externalUuid) - .withDebitedAmount(debitedAmount) - .withLastUpdated(now()) - .withCardId("") - .withPaymentType(paymentType) - ) - ) - .thenRun(_ => - PayInFailed( - "", - Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, - s"$paymentType not supported" - ) ~> replyTo - ) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - - case cmd: PayInCallback => - import cmd._ - state match { - case Some(paymentAccount) => - val clientId = paymentAccount.clientId.orElse( - internalClientId - ) - val paymentProvider = loadPaymentProvider(clientId) - import paymentProvider._ - loadPayInTransaction(orderUuid, transactionId, None) match { - case Some(transaction) => - handlePayIn( - entityId, - orderUuid, - replyTo, - paymentAccount, - registerCard = registerCard, - printReceipt = printReceipt, - transaction - ) - case _ => Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + keyValueDao.addKeyValue(transaction.id, entityId) + } + if (updatedPaymentAccount.legalUser) { + updatedPaymentAccount.getLegalUser.uboDeclaration match { + case Some(declaration) => keyValueDao.addKeyValue(declaration.id, entityId) + case _ => + } } + Effect + .persist( + broadcastEvent( + PaymentAccountCreatedOrUpdatedEvent.defaultInstance + .withLastUpdated(lastUpdated) + .withExternalUuid(updatedPaymentAccount.externalUuid) + .copy(profile = updatedPaymentAccount.profile) + ) :+ + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => { + val result = + if (updated) + PaymentAccountUpdated + else + PaymentAccountCreated + result ~> replyTo + }) case cmd: Refund => import cmd._ @@ -1058,222 +429,6 @@ trait PaymentBehavior case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) } - case cmd: PayOut => - 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 { - case Some(walletId) => - paymentAccount.bankAccount.flatMap(_.id) match { - case Some(bankAccountId) => - val pit = payInTransactionId.orElse( - paymentAccount.transactions - .filter(t => - t.`type` == Transaction.TransactionType.PAYIN && - (t.status.isTransactionCreated || t.status.isTransactionSucceeded) - ) - .find(_.orderUuid == orderUuid) - .map(_.id) - ) - payOut( - Some( - PayOutTransaction.defaultInstance - .withBankAccountId(bankAccountId) - .withDebitedAmount(creditedAmount) - .withOrderUuid(orderUuid) - .withFeesAmount(feesAmount) - .withCurrency(currency) - .withAuthorId(userId) - .withCreditedUserId(userId) - .withDebitedWalletId(walletId) - .copy( - externalReference = externalReference, - payInTransactionId = pit - ) - ) - ) match { - case Some(transaction) => - keyValueDao.addKeyValue(transaction.id, entityId) - val lastUpdated = now() - val updatedPaymentAccount = paymentAccount - .withTransactions( - paymentAccount.transactions.filterNot(_.id == transaction.id) - :+ transaction.copy(clientId = clientId) - ) - .withLastUpdated(lastUpdated) - if (transaction.status.isTransactionFailedForTechnicalReason) { - log.error( - "Order-{} could not be paid out: {} -> {}", - orderUuid, - transaction.id, - asJson(transaction) - ) - Effect - .persist( - broadcastEvent( - PayOutFailedEvent.defaultInstance - .withOrderUuid(orderUuid) - .withResultMessage(transaction.resultMessage) - .withTransaction(transaction) - .copy(externalReference = externalReference) - ) :+ - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => - PayOutFailed( - transaction.id, - transaction.status, - transaction.resultMessage - ) ~> replyTo - ) - } else if ( - transaction.status.isTransactionSucceeded || transaction.status.isTransactionCreated - ) { - log.info( - "Order-{} paid out : {} -> {}", - orderUuid, - transaction.id, - asJson(transaction) - ) - Effect - .persist( - broadcastEvent( - PaidOutEvent.defaultInstance - .withOrderUuid(orderUuid) - .withLastUpdated(lastUpdated) - .withCreditedAccount(paymentAccount.externalUuid) - .withCreditedAmount(creditedAmount) - .withFeesAmount(feesAmount) - .withCurrency(currency) - .withTransactionId(transaction.id) - .withPaymentType(transaction.paymentType) - .copy(externalReference = externalReference) - ) :+ - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => - PaidOut(transaction.id, transaction.status) ~> replyTo - ) - } else { - log.error( - "Order-{} could not be paid out : {} -> {}", - orderUuid, - transaction.id, - asJson(transaction) - ) - Effect - .persist( - broadcastEvent( - PayOutFailedEvent.defaultInstance - .withOrderUuid(orderUuid) - .withResultMessage(transaction.resultMessage) - .withTransaction(transaction) - .copy(externalReference = externalReference) - ) :+ - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => - PayOutFailed( - transaction.id, - transaction.status, - transaction.resultMessage - ) ~> replyTo - ) - } - case _ => - log.error( - "Order-{} could not be paid out: no transaction returned by provider", - orderUuid - ) - Effect - .persist( - broadcastEvent( - PayOutFailedEvent.defaultInstance - .withOrderUuid(orderUuid) - .withResultMessage("no transaction returned by provider") - .copy(externalReference = externalReference) - ) - ) - .thenRun(_ => - PayOutFailed( - "", - Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, - "no transaction returned by provider" - ) ~> replyTo - ) - } - case _ => - Effect - .persist( - broadcastEvent( - PayOutFailedEvent.defaultInstance - .withOrderUuid(orderUuid) - .withResultMessage("no bank account") - .copy(externalReference = externalReference) - ) - ) - .thenRun(_ => - PayOutFailed( - "", - Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, - "no bank account" - ) ~> replyTo - ) - } - case _ => - Effect - .persist( - broadcastEvent( - PayOutFailedEvent.defaultInstance - .withOrderUuid(orderUuid) - .withResultMessage("no wallet id") - .copy(externalReference = externalReference) - ) - ) - .thenRun(_ => - PayOutFailed( - "", - Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, - "no wallet id" - ) ~> replyTo - ) - } - case _ => - Effect - .persist( - broadcastEvent( - PayOutFailedEvent.defaultInstance - .withOrderUuid(orderUuid) - .withResultMessage("no payment provider user id") - .copy(externalReference = externalReference) - ) - ) - .thenRun(_ => - PayOutFailed( - "", - Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, - "no payment provider user id" - ) ~> replyTo - ) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - case cmd: Transfer => import cmd._ state match { @@ -1607,250 +762,64 @@ trait PaymentBehavior val updatedPaymentAccount = paymentAccount .withTransactions( paymentAccount.transactions.filterNot(_.id == transaction.id) - :+ transaction.copy(clientId = clientId) - ) - .withLastUpdated(lastUpdated) - if ( - transaction.status.isTransactionSucceeded || transaction.status.isTransactionCreated - ) { - Effect - .persist( - broadcastEvent( - DirectDebitedEvent.defaultInstance - .withLastUpdated(lastUpdated) - .withCreditedAccount(paymentAccount.externalUuid) - .withDebitedAmount(debitedAmount) - .withFeesAmount(feesAmount) - .withCurrency(currency) - .withTransactionId(transaction.id) - .withTransactionStatus(transaction.status) - .copy(externalReference = externalReference) - ) :+ - PaymentAccountUpsertedEvent.defaultInstance - .withLastUpdated(lastUpdated) - .withDocument(updatedPaymentAccount) - ) - .thenRun(_ => - DirectDebited(transaction.id, transaction.status) ~> replyTo - ) - } else { - Effect - .persist( - broadcastEvent( - DirectDebitFailedEvent.defaultInstance - .withCreditedAccount(paymentAccount.externalUuid) - .withResultMessage(transaction.resultMessage) - .withTransaction(transaction) - .copy(externalReference = externalReference) - ) :+ - PaymentAccountUpsertedEvent.defaultInstance - .withLastUpdated(lastUpdated) - .withDocument(updatedPaymentAccount) - ) - .thenRun(_ => - DirectDebitFailed( - transaction.id, - transaction.status, - transaction.resultMessage - ) ~> replyTo - ) - } - case _ => Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) - } - } else { - Effect.none.thenRun(_ => IllegalMandateStatus ~> replyTo) - } - case _ => Effect.none.thenRun(_ => MandateNotFound ~> replyTo) - } - case _ => Effect.none.thenRun(_ => WalletNotFound ~> replyTo) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - - case cmd: LoadPayInTransaction => - import cmd._ - state match { - case Some(paymentAccount) => - paymentAccount.transactions.find(t => - t.id == transactionId && t.orderUuid == orderUuid - ) match { - case Some(transaction) - if transaction.status.isTransactionSucceeded - || transaction.status.isTransactionFailed - || transaction.status.isTransactionCanceled => - Effect.none.thenRun(_ => - PayInTransactionLoaded(transaction.id, transaction.status, None) ~> replyTo - ) - case Some(transaction) => - val clientId = paymentAccount.clientId - .orElse(cmd.clientId) - .orElse( - internalClientId - ) - val paymentProvider = loadPaymentProvider(clientId) - import paymentProvider._ - loadPayInTransaction(orderUuid, transactionId, None) match { - case Some(t) => - val lastUpdated = now() - val updatedTransaction = transaction - .withStatus(t.status) - .withId(t.id) - .withAuthorId(t.authorId) - .withAmount(t.amount) - .withLastUpdated(lastUpdated) - .withPaymentType(t.paymentType) - .withResultCode(t.resultCode) - .withResultMessage(t.resultMessage) - .copy( - cardId = t.cardId.orElse(transaction.cardId) - ) - val updatedPaymentAccount = paymentAccount - .withTransactions( - paymentAccount.transactions.filterNot(_.id == t.id) - :+ updatedTransaction.copy(clientId = clientId) - ) - .withLastUpdated(lastUpdated) - if (t.status.isTransactionSucceeded || t.status.isTransactionCreated) { - Effect - .persist( - broadcastEvent( - PaidInEvent.defaultInstance - .withOrderUuid(orderUuid) - .withTransactionId(t.id) - .withDebitedAccount(t.authorId) - .withDebitedAmount(t.amount) - .withLastUpdated(lastUpdated) - .withCardId(t.cardId.orElse(transaction.cardId).getOrElse("")) - .withPaymentType(t.paymentType) - ) :+ - PaymentAccountUpsertedEvent.defaultInstance - .withLastUpdated(lastUpdated) - .withDocument(updatedPaymentAccount) - ) - .thenRun(_ => - PayInTransactionLoaded( - transaction.id, - transaction.status, - None - ) ~> replyTo - ) - } else { - Effect - .persist( - broadcastEvent( - PayInFailedEvent.defaultInstance - .withOrderUuid(orderUuid) - .withResultMessage(t.resultMessage) - .withTransaction(updatedTransaction) - ) :+ - PaymentAccountUpsertedEvent.defaultInstance - .withLastUpdated(lastUpdated) - .withDocument(updatedPaymentAccount) - ) - .thenRun(_ => - PayInTransactionLoaded( - transaction.id, - transaction.status, - None - ) ~> replyTo - ) - } - case None => Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) - } - case None => Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - - case cmd: LoadPayOutTransaction => - import cmd._ - state match { - case Some(paymentAccount) => - paymentAccount.transactions.find(t => - t.id == transactionId && t.orderUuid == orderUuid - ) match { - case Some(transaction) - if transaction.status.isTransactionSucceeded - || transaction.status.isTransactionFailed - || transaction.status.isTransactionCanceled => - Effect.none.thenRun(_ => - PayOutTransactionLoaded(transaction.id, transaction.status, None) ~> replyTo - ) - case Some(transaction) => - val clientId = paymentAccount.clientId - .orElse(cmd.clientId) - .orElse( - internalClientId - ) - val paymentProvider = loadPaymentProvider(clientId) - import paymentProvider._ - loadPayOutTransaction(orderUuid, transactionId) match { - case Some(t) => - val lastUpdated = now() - val updatedTransaction = transaction - .withStatus(t.status) - .withLastUpdated(lastUpdated) - .withResultCode(t.resultCode) - .withResultMessage(t.resultMessage) - val updatedPaymentAccount = paymentAccount - .withTransactions( - paymentAccount.transactions.filterNot(_.id == t.id) - :+ updatedTransaction.copy(clientId = clientId) - ) - .withLastUpdated(lastUpdated) - if (t.status.isTransactionSucceeded || t.status.isTransactionCreated) { - Effect - .persist( - broadcastEvent( - PaidOutEvent.defaultInstance - .withOrderUuid(orderUuid) - .withLastUpdated(lastUpdated) - .withCreditedAccount(paymentAccount.externalUuid) - .withCreditedAmount(t.amount) - .withFeesAmount(t.fees) - .withCurrency(t.currency) - .withTransactionId(t.id) - .withPaymentType(t.paymentType) - .copy(externalReference = transaction.externalReference) - ) :+ - PaymentAccountUpsertedEvent.defaultInstance - .withLastUpdated(lastUpdated) - .withDocument(updatedPaymentAccount) - ) - .thenRun(_ => - PayOutTransactionLoaded( - transaction.id, - transaction.status, - None - ) ~> replyTo - ) - } else { - Effect - .persist( - broadcastEvent( - PayOutFailedEvent.defaultInstance - .withOrderUuid(orderUuid) - .withResultMessage(updatedTransaction.resultMessage) - .withTransaction(updatedTransaction) - .copy(externalReference = transaction.externalReference) - ) :+ - PaymentAccountUpsertedEvent.defaultInstance - .withLastUpdated(lastUpdated) - .withDocument(updatedPaymentAccount) - ) - .thenRun(_ => - PayOutTransactionLoaded( - transaction.id, - transaction.status, - None - ) ~> replyTo - ) + :+ transaction.copy(clientId = clientId) + ) + .withLastUpdated(lastUpdated) + if ( + transaction.status.isTransactionSucceeded || transaction.status.isTransactionCreated + ) { + Effect + .persist( + broadcastEvent( + DirectDebitedEvent.defaultInstance + .withLastUpdated(lastUpdated) + .withCreditedAccount(paymentAccount.externalUuid) + .withDebitedAmount(debitedAmount) + .withFeesAmount(feesAmount) + .withCurrency(currency) + .withTransactionId(transaction.id) + .withTransactionStatus(transaction.status) + .copy(externalReference = externalReference) + ) :+ + PaymentAccountUpsertedEvent.defaultInstance + .withLastUpdated(lastUpdated) + .withDocument(updatedPaymentAccount) + ) + .thenRun(_ => + DirectDebited(transaction.id, transaction.status) ~> replyTo + ) + } else { + Effect + .persist( + broadcastEvent( + DirectDebitFailedEvent.defaultInstance + .withCreditedAccount(paymentAccount.externalUuid) + .withResultMessage(transaction.resultMessage) + .withTransaction(transaction) + .copy(externalReference = externalReference) + ) :+ + PaymentAccountUpsertedEvent.defaultInstance + .withLastUpdated(lastUpdated) + .withDocument(updatedPaymentAccount) + ) + .thenRun(_ => + DirectDebitFailed( + transaction.id, + transaction.status, + transaction.resultMessage + ) ~> replyTo + ) + } + case _ => Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) + } + } else { + Effect.none.thenRun(_ => IllegalMandateStatus ~> replyTo) + } + case _ => Effect.none.thenRun(_ => MandateNotFound ~> replyTo) } - case None => Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) + case _ => Effect.none.thenRun(_ => WalletNotFound ~> replyTo) } - case _ => Effect.none.thenRun(_ => TransactionNotFound ~> replyTo) + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) } case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) } @@ -1941,91 +910,6 @@ trait PaymentBehavior case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) } - case cmd: PayInFirstRecurring => - state match { - case Some(paymentAccount) => - paymentAccount.recurryingPayments.find( - _.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) - .withRecurringPayInRegistrationId(cmd.recurringPayInRegistrationId) - .withDebitedAmount(recurringPayment.firstDebitedAmount) - .withFeesAmount(recurringPayment.firstFeesAmount) - .withCurrency(recurringPayment.currency) - .withStatementDescriptor(cmd.statementDescriptor.getOrElse("")) - .withExtension(FirstRecurringPaymentTransaction.first)( - Some( - FirstRecurringPaymentTransaction.defaultInstance.copy( - ipAddress = cmd.ipAddress, - browserInfo = cmd.browserInfo - ) - ) - ) - ) match { - case Some(transaction) => - handleRecurringPayment( - entityId, - replyTo, - paymentAccount, - recurringPayment, - transaction - ) - case _ => - Effect.none.thenRun(_ => - FirstRecurringCardPaymentFailed( - "", - Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, - "no transaction" - ) ~> replyTo - ) - } - case _ => Effect.none.thenRun(_ => RecurringPaymentNotFound ~> replyTo) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - - case cmd: FirstRecurringPaymentCallback => - state match { - case Some(paymentAccount) => - import cmd._ - paymentAccount.recurryingPayments.find(_.getId == recurringPayInRegistrationId) match { - case Some(recurringPayment) => - val clientId = paymentAccount.clientId.orElse( - internalClientId - ) - val paymentProvider = loadPaymentProvider(clientId) - import paymentProvider._ - loadPayInTransaction("", transactionId, Some(recurringPayInRegistrationId)) match { - case Some(transaction) => - handleRecurringPayment( - entityId, - replyTo, - paymentAccount, - recurringPayment, - transaction - ) - case _ => - Effect.none.thenRun(_ => - FirstRecurringCardPaymentFailed( - "", - Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, - "no transaction" - ) ~> replyTo - ) - } - case _ => Effect.none.thenRun(_ => RecurringPaymentNotFound ~> replyTo) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - case cmd: TriggerSchedule4Payment => import cmd.schedule._ if (key.startsWith(nextRecurringPayment)) { @@ -2033,7 +917,7 @@ trait PaymentBehavior case Some(paymentAccount) => val recurringPaymentRegistrationId = key.split("#").last Effect.none.thenRun(_ => { - context.self ! PayNextRecurring( + context.self ! ExecuteNextRecurringPayment( recurringPaymentRegistrationId, paymentAccount.externalUuid ) @@ -2046,180 +930,6 @@ trait PaymentBehavior Effect.none.thenRun(_ => Schedule4PaymentNotTriggered ~> replyTo) } - case cmd: PayNextRecurring => - state match { - case Some(paymentAccount) => - import cmd._ - paymentAccount.recurryingPayments.find( - _.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 - ) - ) - val feesAmount = nextFeesAmount.getOrElse( - recurringPayment.nextFeesAmount.getOrElse( - recurringPayment.firstFeesAmount - ) - ) - val currency = recurringPayment.currency - recurringPayment.`type` match { - case RecurringPayment.RecurringPaymentType.CARD => - recurringPayment.cardStatus match { - case Some(status) if status.isInProgress => // PayIn - createRecurringCardPayment( - RecurringPaymentTransaction.defaultInstance - .withExternalUuid(paymentAccount.externalUuid) - .withDebitedAmount(debitedAmount) - .withFeesAmount(feesAmount) - .withCurrency(currency) - .withStatementDescriptor( - statementDescriptor - .orElse(recurringPayment.statementDescriptor) - .getOrElse("") - ) // TODO - ) match { - case Some(transaction) => - handleRecurringPayment( - entityId, - replyTo, - paymentAccount, - recurringPayment, - transaction - ) - case _ => - val reason = "no transaction" - handleNextRecurringPaymentFailure( - entityId, - replyTo, - paymentAccount, - recurringPayment, - debitedAmount, - feesAmount, - currency, - reason - ) - } - case _ => - val reason = "Illegal recurring payment card status" - handleNextRecurringPaymentFailure( - entityId, - replyTo, - paymentAccount, - recurringPayment, - debitedAmount, - feesAmount, - currency, - reason - ) - } - case _ => // DirectDebit - paymentAccount.userId match { - case Some(creditedUserId) => - paymentAccount.walletId match { - case Some(creditedWalletId) => - paymentAccount.bankAccount.flatMap(_.mandateId) match { - case Some(mandateId) => - if (paymentAccount.mandateActivated) { - directDebit( - Some( - DirectDebitTransaction.defaultInstance - .withAuthorId(creditedUserId) - .withCreditedUserId(creditedUserId) - .withCreditedWalletId(creditedWalletId) - .withDebitedAmount(debitedAmount) - .withFeesAmount(feesAmount) - .withCurrency(currency) - .withMandateId(mandateId) - .withStatementDescriptor(statementDescriptor.getOrElse("")) - ) - ) match { - case Some(transaction) => - handleRecurringPayment( - entityId, - replyTo, - paymentAccount, - recurringPayment, - transaction - ) - case _ => - val reason = "no transaction" - handleNextRecurringPaymentFailure( - entityId, - replyTo, - paymentAccount, - recurringPayment, - debitedAmount, - feesAmount, - currency, - reason - ) - } - } else { - val reason = IllegalMandateStatus.message - handleNextRecurringPaymentFailure( - entityId, - replyTo, - paymentAccount, - recurringPayment, - debitedAmount, - feesAmount, - currency, - reason - ) - } - case _ => - val reason = MandateNotFound.message - handleNextRecurringPaymentFailure( - entityId, - replyTo, - paymentAccount, - recurringPayment, - debitedAmount, - feesAmount, - currency, - reason - ) - } - case _ => - val reason = PaymentAccountNotFound.message - handleNextRecurringPaymentFailure( - entityId, - replyTo, - paymentAccount, - recurringPayment, - debitedAmount, - feesAmount, - currency, - reason - ) - } - case _ => - val reason = PaymentAccountNotFound.message - handleNextRecurringPaymentFailure( - entityId, - replyTo, - paymentAccount, - recurringPayment, - debitedAmount, - feesAmount, - currency, - reason - ) - } - } - case _ => Effect.none.thenRun(_ => RecurringPaymentNotFound ~> replyTo) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - case _: LoadPaymentAccount => state match { case Some(paymentAccount) => @@ -3307,317 +2017,20 @@ trait PaymentBehavior broadcastEvent( PaymentAccountStatusUpdatedEvent.defaultInstance .withExternalUuid(paymentAccount.externalUuid) - .withLastUpdated(lastUpdated) - .withPaymentAccountStatus(PaymentAccount.PaymentAccountStatus.DOCUMENTS_KO) - ) - - Effect - .persist( - events :+ - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => BankAccountDeleted ~> replyTo) - case _ => Effect.none.thenRun(_ => BankAccountNotFound ~> replyTo) - } - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - - case _: LoadCards => - state match { - case Some(paymentAccount) => - Effect.none.thenRun(_ => CardsLoaded(paymentAccount.cards) ~> replyTo) - case _ => Effect.none.thenRun(_ => CardsNotLoaded ~> replyTo) - } - - case cmd: DisableCard => - state match { - case Some(paymentAccount) => - paymentAccount.cards.find(_.id == cmd.cardId) match { - case Some(card) if card.getActive => - paymentAccount.recurryingPayments.find(r => - r.`type`.isCard && r.getCardId == cmd.cardId && - r.nextPaymentDate.isDefined - ) match { - 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() - Effect - .persist( - PaymentAccountUpsertedEvent.defaultInstance - .withDocument( - paymentAccount - .withCards( - paymentAccount.cards.filterNot(_.id == cmd.cardId) :+ card - .withActive(false) - ) - .withLastUpdated(lastUpdated) - ) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => CardDisabled ~> replyTo) - case _ => Effect.none.thenRun(_ => CardNotDisabled ~> replyTo) - } - } - case _ => Effect.none.thenRun(_ => CardNotDisabled ~> replyTo) - } - case _ => Effect.none.thenRun(_ => CardNotDisabled ~> replyTo) - } - - case cmd: RegisterRecurringPayment => - state match { - case Some(paymentAccount) => - cmd.`type` match { - case RecurringPayment.RecurringPaymentType.CARD => // PayIns - paymentAccount.userId match { - case Some(userId) => - paymentAccount.walletId match { - case Some(walletId) => - paymentAccount.cards - .filterNot(_.expired) - .find(_.getActive) - .map(_.id) match { - case Some(cardId) => - val createdDate = now() - var recurringPayment = - RecurringPayment.defaultInstance - .withCreatedDate(createdDate) - .withLastUpdated(createdDate) - .withFirstDebitedAmount(cmd.firstDebitedAmount) - .withFirstFeesAmount(cmd.firstFeesAmount) - .withCurrency(cmd.currency) - .withType(cmd.`type`) - .withCardId(cardId) - .copy( - startDate = cmd.startDate, - endDate = cmd.endDate, - frequency = cmd.frequency, - fixedNextAmount = cmd.fixedNextAmount, - nextDebitedAmount = cmd.nextDebitedAmount, - nextFeesAmount = cmd.nextFeesAmount - ) - val clientId = paymentAccount.clientId - .orElse(cmd.clientId) - .orElse( - internalClientId - ) - val paymentProvider = loadPaymentProvider(clientId) - import paymentProvider._ - registerRecurringCardPayment( - userId, - walletId, - cardId, - recurringPayment - ) match { - case Some(result) => - recurringPayment = recurringPayment - .withId(result.id) - .withCardStatus(result.status) - .copy( - nextRecurringPaymentDate = - recurringPayment.nextPaymentDate.map(_.toDate) - ) - keyValueDao.addKeyValue(recurringPayment.getId, entityId) - Effect - .persist( - broadcastEvent( - RecurringPaymentRegisteredEvent.defaultInstance - .withExternalUuid(paymentAccount.externalUuid) - .withRecurringPayment(recurringPayment) - ) :+ - PaymentAccountUpsertedEvent.defaultInstance - .withDocument( - paymentAccount - .withRecurryingPayments( - paymentAccount.recurryingPayments :+ recurringPayment - ) - .withLastUpdated(createdDate) - ) - .withLastUpdated(createdDate) - ) - .thenRun(_ => RecurringPaymentRegistered(result.id) ~> replyTo) - case _ => - Effect.none.thenRun(_ => RecurringPaymentNotRegistered ~> replyTo) - } - case _ => Effect.none.thenRun(_ => CardNotFound ~> replyTo) - } - case _ => Effect.none.thenRun(_ => WalletNotFound ~> replyTo) - } - case _ => Effect.none.thenRun(_ => UserNotFound ~> replyTo) - } - case _ => // DirectDebits - if (!paymentAccount.mandateActivated) { - Effect.none.thenRun(_ => MandateRequired ~> replyTo) -// paymentAccount.userId match { -// case Some(userId) => -// paymentAccount.bankAccount.flatMap(_.id) match { -// case Some(bankAccountId) => -// addMandate(entityId, replyTo, cmd.debitedAccount, paymentAccount, userId, bankAccountId) -// case _ => Effect.none.thenRun(_ => BankAccountNotFound ~> replyTo) -// } -// case _ => Effect.none.thenRun(_ => UserNotFound ~> replyTo) -// } - } else { - val today = now() - var recurringPayment = - RecurringPayment.defaultInstance - .withId(generateUUID()) - .withCreatedDate(today) - .withLastUpdated(today) - .withFirstDebitedAmount(cmd.firstDebitedAmount) - .withFirstFeesAmount(cmd.firstFeesAmount) - .withCurrency(cmd.currency) - .withType(cmd.`type`) - .copy( - startDate = cmd.startDate, - endDate = cmd.endDate, - frequency = cmd.frequency, - fixedNextAmount = cmd.fixedNextAmount, - nextDebitedAmount = cmd.nextDebitedAmount, - nextFeesAmount = cmd.nextFeesAmount - ) - import app.softnetwork.time._ - val nextDirectDebit: List[ExternalEntityToSchedulerEvent] = - recurringPayment.nextPaymentDate.map(_.toDate) match { - case Some(value) => - recurringPayment = recurringPayment.withNextRecurringPaymentDate(value) - List( - ExternalEntityToSchedulerEvent( - ExternalEntityToSchedulerEvent.Wrapped.AddSchedule( - AddSchedule( - Schedule( - persistenceId, - entityId, - s"$nextRecurringPayment#${recurringPayment.getId}", - 1, - Some(false), - Some(value), - None - ) - ) - ) - ) - ) - case _ => List.empty - } - keyValueDao.addKeyValue(recurringPayment.getId, entityId) - Effect - .persist( - broadcastEvent( - RecurringPaymentRegisteredEvent.defaultInstance - .withExternalUuid(paymentAccount.externalUuid) - .withRecurringPayment(recurringPayment) - ) ++ nextDirectDebit :+ - PaymentAccountUpsertedEvent.defaultInstance - .withDocument( - paymentAccount - .withRecurryingPayments( - paymentAccount.recurryingPayments :+ recurringPayment - ) - .withLastUpdated(today) - ) - .withLastUpdated(today) - ) - .thenRun(_ => RecurringPaymentRegistered(recurringPayment.getId) ~> replyTo) - } - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } - - case cmd: UpdateRecurringCardPaymentRegistration => - state match { - case Some(paymentAccount) => - paymentAccount.recurryingPayments.find( - _.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) => - paymentAccount.cards - .filterNot(_.expired) - .find(card => card.id == cardId && card.getActive) match { - case Some(_) => Some(cardId) - case _ => None - } - case _ => None - } - updateRecurringCardPaymentRegistration( - cmd.recurringPayInRegistrationId, - cardId, - cmd.status - ) match { - case Some(result) => - val lastUpdated = now() - val updatedPaymentAccount = - paymentAccount - .withRecurryingPayments( - paymentAccount.recurryingPayments - .filterNot(_.getId == cmd.recurringPayInRegistrationId) :+ - recurringPayment.withCardStatus(result.status) - ) - .withLastUpdated(lastUpdated) - Effect - .persist( - broadcastEvent( - RecurringPaymentRegisteredEvent.defaultInstance - .withExternalUuid(paymentAccount.externalUuid) - .withRecurringPayment(recurringPayment) - ) ++ { - if (result.status.isEnded) { // cancel scheduled payIn for recurring card payment - List( - ExternalEntityToSchedulerEvent( - ExternalEntityToSchedulerEvent.Wrapped.RemoveSchedule( - RemoveSchedule( - persistenceId, - entityId, - s"$nextRecurringPayment#${cmd.recurringPayInRegistrationId}" - ) - ) - ) - ) - } else { - List.empty - } - } :+ - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => RecurringCardPaymentRegistrationUpdated(result) ~> replyTo) - case _ => - Effect.none.thenRun(_ => RecurringCardPaymentRegistrationNotUpdated ~> replyTo) - } - case _ => - Effect.none.thenRun(_ => RecurringCardPaymentRegistrationNotUpdated ~> replyTo) - } - case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) - } + .withLastUpdated(lastUpdated) + .withPaymentAccountStatus(PaymentAccount.PaymentAccountStatus.DOCUMENTS_KO) + ) - case cmd: LoadRecurringPayment => - state match { - case Some(paymentAccount) => - paymentAccount.recurryingPayments.find( - _.getId == cmd.recurringPaymentRegistrationId - ) match { - case Some(recurringPayment) => - Effect.none.thenRun(_ => RecurringPaymentLoaded(recurringPayment) ~> replyTo) - case _ => Effect.none.thenRun(_ => RecurringPaymentNotFound ~> replyTo) + Effect + .persist( + events :+ + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => BankAccountDeleted ~> replyTo) + case _ => Effect.none.thenRun(_ => BankAccountNotFound ~> replyTo) + } } case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) } @@ -3626,85 +2039,6 @@ trait PaymentBehavior } } - private def handlePayInWithCardPreauthorizedFailure( - orderUuid: String, - replyTo: Option[ActorRef[PaymentResult]], - reason: String - )(implicit context: ActorContext[_]): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { - - Effect - .persist( - broadcastEvent( - PayInFailedEvent.defaultInstance.withOrderUuid(orderUuid).withResultMessage(reason) - ) - ) - .thenRun(_ => PayInWithCardPreAuthorizedFailed(reason) ~> replyTo) - } - - private def handleNextRecurringPaymentFailure( - entityId: String, - replyTo: Option[ActorRef[PaymentResult]], - paymentAccount: PaymentAccount, - recurringPayment: RecurringPayment, - debitedAmount: Int, - feesAmount: Int, - currency: String, - reason: String - )(implicit context: ActorContext[_]): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { - Effect - .persist( - broadcastEvent( - NextRecurringPaymentFailedEvent.defaultInstance - .withDebitedAccount(paymentAccount.externalUuid) - .withResultMessage(reason) - .withDebitedAmount(debitedAmount) - .withFeesAmount(feesAmount) - .withCurrency(currency) - .withRecurringPaymentRegistrationId(recurringPayment.getId) - .withType(recurringPayment.`type`) - .withFrequency(recurringPayment.getFrequency) - .withNumberOfRecurringPayments(recurringPayment.getNumberOfRecurringPayments) - .copy(lastRecurringPaymentDate = recurringPayment.lastRecurringPaymentDate) - ) :+ { - recurringPayment.nextRecurringPaymentDate match { - case Some(value) => - ExternalEntityToSchedulerEvent( - ExternalEntityToSchedulerEvent.Wrapped.AddSchedule( - AddSchedule( - Schedule( - persistenceId, - entityId, - s"$nextRecurringPayment#${recurringPayment.getId}", - 1, - Some(false), - Some(value), - None - ) - ) - ) - ) - case _ => - ExternalEntityToSchedulerEvent( - ExternalEntityToSchedulerEvent.Wrapped.RemoveSchedule( - RemoveSchedule( - persistenceId, - entityId, - s"$nextRecurringPayment#${recurringPayment.getId}" - ) - ) - ) - } - } - ) - .thenRun(_ => - NextRecurringPaymentFailed( - "", - Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, - reason - ) ~> replyTo - ) - } - private def addMandate( entityId: String, replyTo: Option[ActorRef[PaymentResult]], @@ -3781,580 +2115,6 @@ trait PaymentBehavior case _ => super.handleEvent(state, event) } - private[this] def loadPaymentAccount( - entityId: String, - state: Option[PaymentAccount], - 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 => - keyValueDao.lookupKeyValue(uuid) complete () match { - case Success(s) => - s match { - case Some(t) if t != entityId => - log.warn( - s"another payment account entity $t has already been associated with this uuid $uuid" - ) - None - case _ => - keyValueDao.addKeyValue(uuid, entityId) - Some(pa.withUuid(entityId).withCreatedDate(now())) - } - case Failure(f) => - log.error(f.getMessage, f) - None - } - case Some(paymentAccount) => - if (paymentAccount.externalUuid != pa.externalUuid) { - log.warn( - s"the payment account entity $entityId has already been associated with another external uuid ${paymentAccount.externalUuid}" - ) - None - } else { - keyValueDao.addKeyValue(uuid, entityId) - Some( - paymentAccount.copy(clientId = - paymentAccount.clientId - .orElse(clientId) - .orElse(Option(softPayClientSettings.clientId)) - ) - ) - } - } - } - - private[this] def handleRecurringPayment( - entityId: String, - replyTo: Option[ActorRef[PaymentResult]], - paymentAccount: PaymentAccount, - recurringPayment: RecurringPayment, - transaction: Transaction - )(implicit - system: ActorSystem[_], - log: Logger - ): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { - keyValueDao.addKeyValue( - transaction.id, - entityId - ) // add transaction id as a key for this payment account - val lastUpdated = now() - var updatedPaymentAccount = - paymentAccount - .withTransactions( - paymentAccount.transactions - .filterNot(_.id == transaction.id) :+ transaction - ) - .withLastUpdated(lastUpdated) - transaction.status match { - case Transaction.TransactionStatus.TRANSACTION_CREATED - if transaction.redirectUrl.isDefined => // 3ds - Effect - .persist( - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => PaymentRedirection(transaction.redirectUrl.get) ~> replyTo) - case _ => - val first = - recurringPayment.getNumberOfRecurringPayments == 0 && recurringPayment.`type`.isCard - if (transaction.status.isTransactionSucceeded || transaction.status.isTransactionCreated) { - log.debug( - "RecurringPayment-{} succeeded: {} -> {}", - recurringPayment.getId, - transaction.id, - asJson(transaction) - ) - var updatedRecurringPayment = - recurringPayment - .withNumberOfRecurringPayments(recurringPayment.getNumberOfRecurringPayments + 1) - .withLastRecurringPaymentDate(transaction.lastUpdated) - .withLastRecurringPaymentTransactionId(transaction.id) - .withCumulatedDebitedAmount( - recurringPayment.getCumulatedDebitedAmount + transaction.amount - ) - .withCumulatedFeesAmount(recurringPayment.getCumulatedFeesAmount + transaction.fees) - if (recurringPayment.`type`.isCard) { - updatedRecurringPayment = updatedRecurringPayment.withCardStatus( - RecurringPayment.RecurringCardPaymentStatus.IN_PROGRESS - ) - } - updatedRecurringPayment = updatedRecurringPayment.copy( - nextRecurringPaymentDate = updatedRecurringPayment.nextPaymentDate.map(_.toDate) - ) - updatedPaymentAccount = updatedPaymentAccount.withRecurryingPayments( - updatedPaymentAccount.recurryingPayments.filterNot( - _.getId == recurringPayment.getId - ) :+ updatedRecurringPayment - ) - Effect - .persist( - broadcastEvent( - if (first) { - FirstRecurringPaidInEvent.defaultInstance - .withDebitedAccount(paymentAccount.externalUuid) - .withDebitedAmount(transaction.amount) - .withFeesAmount(transaction.fees) - .withCurrency(transaction.currency) - .withTransactionId(transaction.id) - .withFrequency(recurringPayment.getFrequency) - .withRecurringPaymentRegistrationId(recurringPayment.getId) - .withLastUpdated(lastUpdated) - .copy(nextRecurringPaymentDate = - updatedRecurringPayment.nextRecurringPaymentDate - ) - } else { - NextRecurringPaidEvent.defaultInstance - .withDebitedAccount(paymentAccount.externalUuid) - .withDebitedAmount(transaction.amount) - .withFeesAmount(transaction.fees) - .withCurrency(transaction.currency) - .withTransactionId(transaction.id) - .withFrequency(recurringPayment.getFrequency) - .withRecurringPaymentRegistrationId(recurringPayment.getId) - .withType(recurringPayment.`type`) - .withNumberOfRecurringPayments( - updatedRecurringPayment.getNumberOfRecurringPayments - ) - .withCumulatedDebitedAmount(updatedRecurringPayment.getCumulatedDebitedAmount) - .withCumulatedFeesAmount(updatedRecurringPayment.getCumulatedFeesAmount) - .withLastUpdated(lastUpdated) - .copy(nextRecurringPaymentDate = - updatedRecurringPayment.nextRecurringPaymentDate - ) - } - ) :+ { - updatedRecurringPayment.nextRecurringPaymentDate match { - case Some(value) => - ExternalEntityToSchedulerEvent( - ExternalEntityToSchedulerEvent.Wrapped.AddSchedule( - AddSchedule( - Schedule( - persistenceId, - entityId, - s"$nextRecurringPayment#${recurringPayment.getId}", - 1, - Some(false), - Some(value), - None - ) - ) - ) - ) - case _ => - ExternalEntityToSchedulerEvent( - ExternalEntityToSchedulerEvent.Wrapped.RemoveSchedule( - RemoveSchedule( - persistenceId, - entityId, - s"$nextRecurringPayment#${recurringPayment.getId}" - ) - ) - ) - } - } :+ - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => - (if (first) FirstRecurringPaidIn(transaction.id, transaction.status) - else NextRecurringPaid(transaction.id, transaction.status)) ~> replyTo - ) - } else { - log.error( - "RecurringPayment-{} failed: {} -> {}", - recurringPayment.getId, - transaction.id, - asJson(transaction) - ) - Effect - .persist( - broadcastEvent( - if (first) { - FirstRecurringCardPaymentFailedEvent.defaultInstance - .withDebitedAccount(paymentAccount.externalUuid) - .withResultMessage(transaction.getReasonMessage) - .withDebitedAmount(transaction.amount) - .withFeesAmount(transaction.fees) - .withCurrency(transaction.currency) - .withTransaction(transaction) - .withRecurringPaymentRegistrationId(recurringPayment.getId) - .withFrequency(recurringPayment.getFrequency) - } else { - NextRecurringPaymentFailedEvent.defaultInstance - .withDebitedAccount(paymentAccount.externalUuid) - .withResultMessage(transaction.getReasonMessage) - .withDebitedAmount(transaction.amount) - .withFeesAmount(transaction.fees) - .withCurrency(transaction.currency) - .withTransaction(transaction) - .withRecurringPaymentRegistrationId(recurringPayment.getId) - .withType(recurringPayment.`type`) - .withFrequency(recurringPayment.getFrequency) - .withNumberOfRecurringPayments(recurringPayment.getNumberOfRecurringPayments) - .copy(lastRecurringPaymentDate = recurringPayment.lastRecurringPaymentDate) - } - ) :+ { - recurringPayment.nextRecurringPaymentDate match { - case Some(value) => - ExternalEntityToSchedulerEvent( - ExternalEntityToSchedulerEvent.Wrapped.AddSchedule( - AddSchedule( - Schedule( - persistenceId, - entityId, - s"$nextRecurringPayment#${recurringPayment.getId}", - 1, - Some(false), - Some(value), - None - ) - ) - ) - ) - case _ => - ExternalEntityToSchedulerEvent( - ExternalEntityToSchedulerEvent.Wrapped.RemoveSchedule( - RemoveSchedule( - persistenceId, - entityId, - s"$nextRecurringPayment#${recurringPayment.getId}" - ) - ) - ) - } - } :+ - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => - ( - if (first) - FirstRecurringCardPaymentFailed( - transaction.id, - transaction.status, - transaction.getReasonMessage - ) - else - NextRecurringPaymentFailed( - transaction.id, - transaction.status, - transaction.getReasonMessage - ) - ) ~> replyTo - ) - } - } - } - - private[this] def handlePayIn( - entityId: String, - orderUuid: String, - replyTo: Option[ActorRef[PaymentResult]], - paymentAccount: PaymentAccount, - registerCard: Boolean, - printReceipt: Boolean, - transaction: Transaction, - registerWallet: Boolean = false - )(implicit - system: ActorSystem[_], - log: Logger, - softPayClientSettings: SoftPayClientSettings - ): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { - keyValueDao.addKeyValue( - transaction.id, - entityId - ) // add transaction id as a key for this payment account - val lastUpdated = now() - var updatedPaymentAccount = - paymentAccount - .withTransactions( - paymentAccount.transactions - .filterNot(_.id == transaction.id) :+ transaction - ) - .withLastUpdated(lastUpdated) - val walletEvents: List[ExternalSchedulerEvent] = - if (registerWallet) { - broadcastEvent( - WalletRegisteredEvent.defaultInstance - .withOrderUuid(orderUuid) - .withExternalUuid(paymentAccount.externalUuid) - .withLastUpdated(lastUpdated) - .copy( - userId = paymentAccount.userId.get, - walletId = paymentAccount.walletId.get - ) - ) - } else { - List.empty - } - transaction.status match { - case Transaction.TransactionStatus.TRANSACTION_PENDING_PAYMENT - if transaction.paymentClientReturnUrl.isDefined => - Effect - .persist( - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) +: walletEvents - ) - .thenRun(_ => - PaymentRequired( - transaction.id, - transaction.paymentClientSecret.getOrElse(""), - transaction.paymentClientData.getOrElse(""), - transaction.paymentClientReturnUrl.get - ) ~> replyTo - ) - case Transaction.TransactionStatus.TRANSACTION_CREATED - if transaction.redirectUrl.isDefined => // 3ds | PayPal - Effect - .persist( - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) +: walletEvents - ) - .thenRun(_ => PaymentRedirection(transaction.redirectUrl.get) ~> replyTo) - 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 { - case Some(cardId) => - loadCard(cardId) match { - case Some(card) => - val updatedCard = updatedPaymentAccount.maybeUser match { - case Some(user) => - card - .withFirstName(user.firstName) - .withLastName(user.lastName) - .withBirthday(user.birthday) - case _ => card - } - updatedPaymentAccount = updatedPaymentAccount.withCards( - updatedPaymentAccount.cards.filterNot(_.id == updatedCard.id) :+ updatedCard - ) - broadcastEvent( - CardRegisteredEvent.defaultInstance - .withOrderUuid(orderUuid) - .withExternalUuid(paymentAccount.externalUuid) - .withCard(updatedCard) - .withLastUpdated(lastUpdated) - ) - case _ => List.empty - } - case _ => List.empty - } - } else { - List.empty - } - updatedPaymentAccount = transaction.preAuthorizationId match { - case Some(preAuthorizationId) => - transaction.preAuthorizationDebitedAmount match { - case Some(preAuthorizationDebitedAmount) - if transaction.amount < preAuthorizationDebitedAmount => - // validation required - val updatedTransaction = updatedPaymentAccount.transactions - .find(_.id == preAuthorizationId) - .map( - _.copy( - preAuthorizationValidated = - Some(validatePreAuthorization(transaction.orderUuid, preAuthorizationId)), - clientId = clientId - ) - ) - updatedPaymentAccount.withTransactions( - updatedPaymentAccount.transactions.filterNot(_.id == preAuthorizationId) ++ Seq( - updatedTransaction - ).flatten - ) - case _ => updatedPaymentAccount - } - case _ => updatedPaymentAccount - } - Effect - .persist( - registerCardEvents ++ - broadcastEvent( - PaidInEvent.defaultInstance - .withOrderUuid(orderUuid) - .withTransactionId(transaction.id) - .withDebitedAccount(paymentAccount.externalUuid) - .withDebitedAmount(transaction.amount) - .withLastUpdated(lastUpdated) - .withCardId(transaction.cardId.getOrElse("")) - .withPaymentType(transaction.paymentType) - .withPrintReceipt(printReceipt) - ) ++ - (PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) +: walletEvents) - ) - .thenRun(_ => PaidIn(transaction.id, transaction.status) ~> replyTo) - } else { - log.error( - "Order-{} could not be paid in: {} -> {}", - orderUuid, - transaction.id, - asJson(transaction) - ) - Effect - .persist( - broadcastEvent( - PayInFailedEvent.defaultInstance - .withOrderUuid(orderUuid) - .withResultMessage(transaction.resultMessage) - .withTransaction(transaction) - ) ++ - (PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) +: walletEvents) - ) - .thenRun(_ => - PayInFailed(transaction.id, transaction.status, transaction.resultMessage) ~> replyTo - ) - } - } - } - - private[this] def handleCardPreAuthorization( - entityId: String, - orderUuid: String, - replyTo: Option[ActorRef[PaymentResult]], - paymentAccount: PaymentAccount, - registerCard: Boolean, - printReceipt: Boolean, - transaction: Transaction - )(implicit - system: ActorSystem[_], - log: Logger, - softPayClientSettings: SoftPayClientSettings - ): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { - keyValueDao.addKeyValue( - transaction.id, - entityId - ) // add transaction id as a key for this payment account - val lastUpdated = now() - var updatedPaymentAccount = - paymentAccount - .withTransactions( - paymentAccount.transactions - .filterNot(_.id == transaction.id) :+ transaction - .copy(clientId = paymentAccount.clientId) - ) - .withLastUpdated(lastUpdated) - transaction.status match { - case Transaction.TransactionStatus.TRANSACTION_CREATED - if transaction.redirectUrl.isDefined => // 3ds - Effect - .persist( - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => - PaymentRedirection( - transaction.redirectUrl.get - ) ~> replyTo - ) - case _ => - if (transaction.status.isTransactionSucceeded || transaction.status.isTransactionCreated) { - log.debug( - "Order-{} pre authorized: {} -> {}", - 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 { - case Some(cardId) => - loadCard(cardId) match { - case Some(card) => - val updatedCard = updatedPaymentAccount.maybeUser match { - case Some(user) => - card - .withFirstName(user.firstName) - .withLastName(user.lastName) - .withBirthday(user.birthday) - case _ => card - } - updatedPaymentAccount = updatedPaymentAccount.withCards( - updatedPaymentAccount.cards.filterNot(_.id == updatedCard.id) :+ updatedCard - ) - broadcastEvent( - CardRegisteredEvent.defaultInstance - .withOrderUuid(orderUuid) - .withExternalUuid(paymentAccount.externalUuid) - .withCard(updatedCard) - .withLastUpdated(lastUpdated) - ) - case _ => List.empty - } - case _ => List.empty - } - } else { - List.empty - } - Effect - .persist( - registerCardEvents ++ - broadcastEvent( - CardPreAuthorizedEvent.defaultInstance - .withOrderUuid(orderUuid) - .withTransactionId(transaction.id) - .withCardId(transaction.getCardId) - .withDebitedAccount(paymentAccount.externalUuid) - .withDebitedAmount(transaction.amount) - .withLastUpdated(lastUpdated) - .withPrintReceipt(printReceipt) - ) :+ - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => CardPreAuthorized(transaction.id) ~> replyTo) - } else { - log.error( - "Order-{} could not be pre authorized: {} -> {}", - orderUuid, - transaction.id, - asJson(transaction) - ) - Effect - .persist( - broadcastEvent( - CardPreAuthorizationFailedEvent.defaultInstance - .withOrderUuid(orderUuid) - .withResultMessage(transaction.resultMessage) - .withTransaction(transaction) - ) :+ - PaymentAccountUpsertedEvent.defaultInstance - .withDocument(updatedPaymentAccount) - .withLastUpdated(lastUpdated) - ) - .thenRun(_ => CardPreAuthorizationFailed(transaction.resultMessage) ~> replyTo) - } - } - } - private[this] def updateDocumentStatus( paymentAccount: PaymentAccount, document: KycDocument, @@ -4534,20 +2294,6 @@ trait PaymentBehavior 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/PaymentCommandHandler.scala b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentCommandHandler.scala new file mode 100644 index 0000000..ff31965 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentCommandHandler.scala @@ -0,0 +1,89 @@ +package app.softnetwork.payment.persistence.typed + +import akka.actor.typed.ActorSystem +import app.softnetwork.kv.handlers.GenericKeyValueDao +import app.softnetwork.payment.annotation.InternalApi +import app.softnetwork.payment.api.config.SoftPayClientSettings +import app.softnetwork.payment.handlers.{PaymentDao, PaymentKvDao, SoftPayAccountDao} +import app.softnetwork.payment.model.PaymentAccount +import app.softnetwork.payment.spi.{PaymentProvider, PaymentProviders} +import app.softnetwork.persistence.now +import app.softnetwork.concurrent.Completion +import app.softnetwork.time._ +import org.slf4j.Logger + +import scala.util.{Failure, Success} + +trait PaymentCommandHandler { _: Completion => + + lazy val keyValueDao: GenericKeyValueDao = PaymentKvDao + + def paymentDao: PaymentDao + + def softPayAccountDao: SoftPayAccountDao + + @InternalApi + private[payment] def loadPaymentAccount( + entityId: String, + state: Option[PaymentAccount], + 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 => + keyValueDao.lookupKeyValue(uuid) complete () match { + case Success(s) => + s match { + case Some(t) if t != entityId => + log.warn( + s"another payment account entity $t has already been associated with this uuid $uuid" + ) + None + case _ => + keyValueDao.addKeyValue(uuid, entityId) + Some(pa.withUuid(entityId).withCreatedDate(now())) + } + case Failure(f) => + log.error(f.getMessage, f) + None + } + case Some(paymentAccount) => + if (paymentAccount.externalUuid != pa.externalUuid) { + log.warn( + s"the payment account entity $entityId has already been associated with another external uuid ${paymentAccount.externalUuid}" + ) + None + } else { + keyValueDao.addKeyValue(uuid, entityId) + Some( + paymentAccount.copy(clientId = + paymentAccount.clientId + .orElse(clientId) + .orElse(Option(softPayClientSettings.clientId)) + ) + ) + } + } + } + + @InternalApi + private[payment] 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")) + ) + } + +} diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentTimers.scala b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentTimers.scala new file mode 100644 index 0000000..5c53e2d --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentTimers.scala @@ -0,0 +1,7 @@ +package app.softnetwork.payment.persistence.typed + +trait PaymentTimers { + + val nextRecurringPayment: String = "NextRecurringPayment" + +} diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/typed/RecurringPaymentCommandHandler.scala b/core/src/main/scala/app/softnetwork/payment/persistence/typed/RecurringPaymentCommandHandler.scala new file mode 100644 index 0000000..8169ae3 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/payment/persistence/typed/RecurringPaymentCommandHandler.scala @@ -0,0 +1,894 @@ +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.concurrent.Completion +import app.softnetwork.payment.api.config.SoftPayClientSettings +import app.softnetwork.payment.message.PaymentEvents.{ + PaymentAccountUpsertedEvent, + RecurringPaymentRegisteredEvent +} +import app.softnetwork.payment.message.PaymentMessages.{ + CardNotFound, + ExecuteFirstRecurringPayment, + ExecuteNextRecurringPayment, + FirstRecurringCardPaymentFailed, + FirstRecurringPaidIn, + FirstRecurringPaymentCallback, + IllegalMandateStatus, + LoadRecurringPayment, + MandateNotFound, + MandateRequired, + NextRecurringPaid, + NextRecurringPaymentFailed, + PaymentAccountNotFound, + PaymentRedirection, + PaymentResult, + RecurringCardPaymentRegistrationNotUpdated, + RecurringCardPaymentRegistrationUpdated, + RecurringPaymentCommand, + RecurringPaymentLoaded, + RecurringPaymentNotFound, + RecurringPaymentNotRegistered, + RecurringPaymentRegistered, + RegisterRecurringPayment, + UpdateRecurringCardPaymentRegistration, + UserNotFound, + WalletNotFound +} +import app.softnetwork.payment.message.TransactionEvents.{ + FirstRecurringCardPaymentFailedEvent, + FirstRecurringPaidInEvent, + NextRecurringPaidEvent, + NextRecurringPaymentFailedEvent +} +import app.softnetwork.payment.model.{ + DirectDebitTransaction, + FirstRecurringPaymentTransaction, + PaymentAccount, + RecurringPayment, + RecurringPaymentTransaction, + Transaction +} +import app.softnetwork.persistence.{generateUUID, now} +import app.softnetwork.persistence.typed._ +import app.softnetwork.scheduler.message.{AddSchedule, RemoveSchedule} +import app.softnetwork.scheduler.message.SchedulerEvents.{ + ExternalEntityToSchedulerEvent, + ExternalSchedulerEvent +} +import app.softnetwork.scheduler.model.Schedule +import app.softnetwork.serialization.asJson +import app.softnetwork.time._ +import org.slf4j.Logger + +trait RecurringPaymentCommandHandler + extends EntityCommandHandler[ + RecurringPaymentCommand, + PaymentAccount, + ExternalSchedulerEvent, + PaymentResult + ] + with PaymentCommandHandler + with PaymentTimers + with Completion { + + def persistenceId: String + + override def apply( + entityId: String, + state: Option[PaymentAccount], + command: RecurringPaymentCommand, + replyTo: Option[ActorRef[PaymentResult]], + timers: TimerScheduler[RecurringPaymentCommand] + )(implicit + context: ActorContext[RecurringPaymentCommand] + ): 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: RegisterRecurringPayment => + state match { + case Some(paymentAccount) => + cmd.`type` match { + case RecurringPayment.RecurringPaymentType.CARD => // PayIns + paymentAccount.userId match { + case Some(userId) => + paymentAccount.walletId match { + case Some(walletId) => + paymentAccount.cards + .filterNot(_.expired) + .find(_.getActive) + .map(_.id) match { + case Some(cardId) => + val createdDate = now() + var recurringPayment = + RecurringPayment.defaultInstance + .withCreatedDate(createdDate) + .withLastUpdated(createdDate) + .withFirstDebitedAmount(cmd.firstDebitedAmount) + .withFirstFeesAmount(cmd.firstFeesAmount) + .withCurrency(cmd.currency) + .withType(cmd.`type`) + .withCardId(cardId) + .copy( + startDate = cmd.startDate, + endDate = cmd.endDate, + frequency = cmd.frequency, + fixedNextAmount = cmd.fixedNextAmount, + nextDebitedAmount = cmd.nextDebitedAmount, + nextFeesAmount = cmd.nextFeesAmount + ) + val clientId = paymentAccount.clientId + .orElse(cmd.clientId) + .orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ + registerRecurringCardPayment( + userId, + walletId, + cardId, + recurringPayment + ) match { + case Some(result) => + recurringPayment = recurringPayment + .withId(result.id) + .withCardStatus(result.status) + .copy( + nextRecurringPaymentDate = + recurringPayment.nextPaymentDate.map(_.toDate) + ) + keyValueDao.addKeyValue(recurringPayment.getId, entityId) + Effect + .persist( + List( + RecurringPaymentRegisteredEvent.defaultInstance + .withExternalUuid(paymentAccount.externalUuid) + .withRecurringPayment(recurringPayment) + ) :+ + PaymentAccountUpsertedEvent.defaultInstance + .withDocument( + paymentAccount + .withRecurryingPayments( + paymentAccount.recurryingPayments :+ recurringPayment + ) + .withLastUpdated(createdDate) + ) + .withLastUpdated(createdDate) + ) + .thenRun(_ => RecurringPaymentRegistered(result.id) ~> replyTo) + case _ => + Effect.none.thenRun(_ => RecurringPaymentNotRegistered ~> replyTo) + } + case _ => Effect.none.thenRun(_ => CardNotFound ~> replyTo) + } + case _ => Effect.none.thenRun(_ => WalletNotFound ~> replyTo) + } + case _ => Effect.none.thenRun(_ => UserNotFound ~> replyTo) + } + case _ => // DirectDebits + if (!paymentAccount.mandateActivated) { + Effect.none.thenRun(_ => MandateRequired ~> replyTo) + // paymentAccount.userId match { + // case Some(userId) => + // paymentAccount.bankAccount.flatMap(_.id) match { + // case Some(bankAccountId) => + // addMandate(entityId, replyTo, cmd.debitedAccount, paymentAccount, userId, bankAccountId) + // case _ => Effect.none.thenRun(_ => BankAccountNotFound ~> replyTo) + // } + // case _ => Effect.none.thenRun(_ => UserNotFound ~> replyTo) + // } + } else { + val today = now() + var recurringPayment = + RecurringPayment.defaultInstance + .withId(generateUUID()) + .withCreatedDate(today) + .withLastUpdated(today) + .withFirstDebitedAmount(cmd.firstDebitedAmount) + .withFirstFeesAmount(cmd.firstFeesAmount) + .withCurrency(cmd.currency) + .withType(cmd.`type`) + .copy( + startDate = cmd.startDate, + endDate = cmd.endDate, + frequency = cmd.frequency, + fixedNextAmount = cmd.fixedNextAmount, + nextDebitedAmount = cmd.nextDebitedAmount, + nextFeesAmount = cmd.nextFeesAmount + ) + import app.softnetwork.time._ + val nextDirectDebit: List[ExternalEntityToSchedulerEvent] = + recurringPayment.nextPaymentDate.map(_.toDate) match { + case Some(value) => + recurringPayment = recurringPayment.withNextRecurringPaymentDate(value) + List( + ExternalEntityToSchedulerEvent( + ExternalEntityToSchedulerEvent.Wrapped.AddSchedule( + AddSchedule( + Schedule( + persistenceId, + entityId, + s"$nextRecurringPayment#${recurringPayment.getId}", + 1, + Some(false), + Some(value), + None + ) + ) + ) + ) + ) + case _ => List.empty + } + keyValueDao.addKeyValue(recurringPayment.getId, entityId) + Effect + .persist( + List( + RecurringPaymentRegisteredEvent.defaultInstance + .withExternalUuid(paymentAccount.externalUuid) + .withRecurringPayment(recurringPayment) + ) ++ nextDirectDebit :+ + PaymentAccountUpsertedEvent.defaultInstance + .withDocument( + paymentAccount + .withRecurryingPayments( + paymentAccount.recurryingPayments :+ recurringPayment + ) + .withLastUpdated(today) + ) + .withLastUpdated(today) + ) + .thenRun(_ => RecurringPaymentRegistered(recurringPayment.getId) ~> replyTo) + } + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + case cmd: UpdateRecurringCardPaymentRegistration => + state match { + case Some(paymentAccount) => + paymentAccount.recurryingPayments.find( + _.getId == cmd.recurringPaymentRegistrationId + ) 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) => + paymentAccount.cards + .filterNot(_.expired) + .find(card => card.id == cardId && card.getActive) match { + case Some(_) => Some(cardId) + case _ => None + } + case _ => None + } + updateRecurringCardPaymentRegistration( + cmd.recurringPaymentRegistrationId, + cardId, + cmd.status + ) match { + case Some(result) => + val lastUpdated = now() + val updatedPaymentAccount = + paymentAccount + .withRecurryingPayments( + paymentAccount.recurryingPayments + .filterNot(_.getId == cmd.recurringPaymentRegistrationId) :+ + recurringPayment.withCardStatus(result.status) + ) + .withLastUpdated(lastUpdated) + Effect + .persist( + List( + RecurringPaymentRegisteredEvent.defaultInstance + .withExternalUuid(paymentAccount.externalUuid) + .withRecurringPayment(recurringPayment) + ) ++ { + if (result.status.isEnded) { // cancel scheduled payIn for recurring card payment + List( + ExternalEntityToSchedulerEvent( + ExternalEntityToSchedulerEvent.Wrapped.RemoveSchedule( + RemoveSchedule( + persistenceId, + entityId, + s"$nextRecurringPayment#${cmd.recurringPaymentRegistrationId}" + ) + ) + ) + ) + } else { + List.empty + } + } :+ + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => RecurringCardPaymentRegistrationUpdated(result) ~> replyTo) + case _ => + Effect.none.thenRun(_ => RecurringCardPaymentRegistrationNotUpdated ~> replyTo) + } + case _ => + Effect.none.thenRun(_ => RecurringCardPaymentRegistrationNotUpdated ~> replyTo) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + case cmd: LoadRecurringPayment => + state match { + case Some(paymentAccount) => + paymentAccount.recurryingPayments.find( + _.getId == cmd.recurringPaymentRegistrationId + ) match { + case Some(recurringPayment) => + Effect.none.thenRun(_ => RecurringPaymentLoaded(recurringPayment) ~> replyTo) + case _ => Effect.none.thenRun(_ => RecurringPaymentNotFound ~> replyTo) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + case cmd: ExecuteFirstRecurringPayment => + state match { + case Some(paymentAccount) => + paymentAccount.recurryingPayments.find( + _.getId == cmd.recurringPaymentRegistrationId + ) match { + case Some(recurringPayment) => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ + createRecurringCardPayment( + RecurringPaymentTransaction.defaultInstance + .withExternalUuid(paymentAccount.externalUuid) + .withRecurringPaymentRegistrationId(cmd.recurringPaymentRegistrationId) + .withDebitedAmount(recurringPayment.firstDebitedAmount) + .withFeesAmount(recurringPayment.firstFeesAmount) + .withCurrency(recurringPayment.currency) + .withStatementDescriptor(cmd.statementDescriptor.getOrElse("")) + .withExtension(FirstRecurringPaymentTransaction.first)( + Some( + FirstRecurringPaymentTransaction.defaultInstance.copy( + ipAddress = cmd.ipAddress, + browserInfo = cmd.browserInfo + ) + ) + ) + ) match { + case Some(transaction) => + handleRecurringPayment( + entityId, + replyTo, + paymentAccount, + recurringPayment, + transaction + ) + case _ => + Effect.none.thenRun(_ => + FirstRecurringCardPaymentFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + "no transaction" + ) ~> replyTo + ) + } + case _ => Effect.none.thenRun(_ => RecurringPaymentNotFound ~> replyTo) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + case cmd: FirstRecurringPaymentCallback => + state match { + case Some(paymentAccount) => + import cmd._ + paymentAccount.recurryingPayments.find(_.getId == recurringPayInRegistrationId) match { + case Some(recurringPayment) => + val clientId = paymentAccount.clientId.orElse( + internalClientId + ) + val paymentProvider = loadPaymentProvider(clientId) + import paymentProvider._ + loadPayInTransaction("", transactionId, Some(recurringPayInRegistrationId)) match { + case Some(transaction) => + handleRecurringPayment( + entityId, + replyTo, + paymentAccount, + recurringPayment, + transaction + ) + case _ => + Effect.none.thenRun(_ => + FirstRecurringCardPaymentFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + "no transaction" + ) ~> replyTo + ) + } + case _ => Effect.none.thenRun(_ => RecurringPaymentNotFound ~> replyTo) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + case cmd: ExecuteNextRecurringPayment => + state match { + case Some(paymentAccount) => + import cmd._ + paymentAccount.recurryingPayments.find( + _.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 + ) + ) + val feesAmount = nextFeesAmount.getOrElse( + recurringPayment.nextFeesAmount.getOrElse( + recurringPayment.firstFeesAmount + ) + ) + val currency = recurringPayment.currency + recurringPayment.`type` match { + case RecurringPayment.RecurringPaymentType.CARD => + recurringPayment.cardStatus match { + case Some(status) if status.isInProgress => // PayIn + createRecurringCardPayment( + RecurringPaymentTransaction.defaultInstance + .withExternalUuid(paymentAccount.externalUuid) + .withDebitedAmount(debitedAmount) + .withFeesAmount(feesAmount) + .withCurrency(currency) + .withStatementDescriptor( + statementDescriptor + .orElse(recurringPayment.statementDescriptor) + .getOrElse("") + ) // TODO + ) match { + case Some(transaction) => + handleRecurringPayment( + entityId, + replyTo, + paymentAccount, + recurringPayment, + transaction + ) + case _ => + val reason = "no transaction" + handleNextRecurringPaymentFailure( + entityId, + replyTo, + paymentAccount, + recurringPayment, + debitedAmount, + feesAmount, + currency, + reason + ) + } + case _ => + val reason = "Illegal recurring payment card status" + handleNextRecurringPaymentFailure( + entityId, + replyTo, + paymentAccount, + recurringPayment, + debitedAmount, + feesAmount, + currency, + reason + ) + } + case _ => // DirectDebit + paymentAccount.userId match { + case Some(creditedUserId) => + paymentAccount.walletId match { + case Some(creditedWalletId) => + paymentAccount.bankAccount.flatMap(_.mandateId) match { + case Some(mandateId) => + if (paymentAccount.mandateActivated) { + directDebit( + Some( + DirectDebitTransaction.defaultInstance + .withAuthorId(creditedUserId) + .withCreditedUserId(creditedUserId) + .withCreditedWalletId(creditedWalletId) + .withDebitedAmount(debitedAmount) + .withFeesAmount(feesAmount) + .withCurrency(currency) + .withMandateId(mandateId) + .withStatementDescriptor(statementDescriptor.getOrElse("")) + ) + ) match { + case Some(transaction) => + handleRecurringPayment( + entityId, + replyTo, + paymentAccount, + recurringPayment, + transaction + ) + case _ => + val reason = "no transaction" + handleNextRecurringPaymentFailure( + entityId, + replyTo, + paymentAccount, + recurringPayment, + debitedAmount, + feesAmount, + currency, + reason + ) + } + } else { + val reason = IllegalMandateStatus.message + handleNextRecurringPaymentFailure( + entityId, + replyTo, + paymentAccount, + recurringPayment, + debitedAmount, + feesAmount, + currency, + reason + ) + } + case _ => + val reason = MandateNotFound.message + handleNextRecurringPaymentFailure( + entityId, + replyTo, + paymentAccount, + recurringPayment, + debitedAmount, + feesAmount, + currency, + reason + ) + } + case _ => + val reason = PaymentAccountNotFound.message + handleNextRecurringPaymentFailure( + entityId, + replyTo, + paymentAccount, + recurringPayment, + debitedAmount, + feesAmount, + currency, + reason + ) + } + case _ => + val reason = PaymentAccountNotFound.message + handleNextRecurringPaymentFailure( + entityId, + replyTo, + paymentAccount, + recurringPayment, + debitedAmount, + feesAmount, + currency, + reason + ) + } + } + case _ => Effect.none.thenRun(_ => RecurringPaymentNotFound ~> replyTo) + } + case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo) + } + + } + } + + private[this] def handleRecurringPayment( + entityId: String, + replyTo: Option[ActorRef[PaymentResult]], + paymentAccount: PaymentAccount, + recurringPayment: RecurringPayment, + transaction: Transaction + )(implicit + system: ActorSystem[_], + log: Logger + ): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { + keyValueDao.addKeyValue( + transaction.id, + entityId + ) // add transaction id as a key for this payment account + val lastUpdated = now() + var updatedPaymentAccount = + paymentAccount + .withTransactions( + paymentAccount.transactions + .filterNot(_.id == transaction.id) :+ transaction + ) + .withLastUpdated(lastUpdated) + transaction.status match { + case Transaction.TransactionStatus.TRANSACTION_CREATED + if transaction.redirectUrl.isDefined => // 3ds + Effect + .persist( + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => PaymentRedirection(transaction.redirectUrl.get) ~> replyTo) + case _ => + val first = + recurringPayment.getNumberOfRecurringPayments == 0 && recurringPayment.`type`.isCard + if (transaction.status.isTransactionSucceeded || transaction.status.isTransactionCreated) { + log.debug( + "RecurringPayment-{} succeeded: {} -> {}", + recurringPayment.getId, + transaction.id, + asJson(transaction) + ) + var updatedRecurringPayment = + recurringPayment + .withNumberOfRecurringPayments(recurringPayment.getNumberOfRecurringPayments + 1) + .withLastRecurringPaymentDate(transaction.lastUpdated) + .withLastRecurringPaymentTransactionId(transaction.id) + .withCumulatedDebitedAmount( + recurringPayment.getCumulatedDebitedAmount + transaction.amount + ) + .withCumulatedFeesAmount(recurringPayment.getCumulatedFeesAmount + transaction.fees) + if (recurringPayment.`type`.isCard) { + updatedRecurringPayment = updatedRecurringPayment.withCardStatus( + RecurringPayment.RecurringCardPaymentStatus.IN_PROGRESS + ) + } + updatedRecurringPayment = updatedRecurringPayment.copy( + nextRecurringPaymentDate = updatedRecurringPayment.nextPaymentDate.map(_.toDate) + ) + updatedPaymentAccount = updatedPaymentAccount.withRecurryingPayments( + updatedPaymentAccount.recurryingPayments.filterNot( + _.getId == recurringPayment.getId + ) :+ updatedRecurringPayment + ) + Effect + .persist( + List( + if (first) { + FirstRecurringPaidInEvent.defaultInstance + .withDebitedAccount(paymentAccount.externalUuid) + .withDebitedAmount(transaction.amount) + .withFeesAmount(transaction.fees) + .withCurrency(transaction.currency) + .withTransactionId(transaction.id) + .withFrequency(recurringPayment.getFrequency) + .withRecurringPaymentRegistrationId(recurringPayment.getId) + .withLastUpdated(lastUpdated) + .copy(nextRecurringPaymentDate = + updatedRecurringPayment.nextRecurringPaymentDate + ) + } else { + NextRecurringPaidEvent.defaultInstance + .withDebitedAccount(paymentAccount.externalUuid) + .withDebitedAmount(transaction.amount) + .withFeesAmount(transaction.fees) + .withCurrency(transaction.currency) + .withTransactionId(transaction.id) + .withFrequency(recurringPayment.getFrequency) + .withRecurringPaymentRegistrationId(recurringPayment.getId) + .withType(recurringPayment.`type`) + .withNumberOfRecurringPayments( + updatedRecurringPayment.getNumberOfRecurringPayments + ) + .withCumulatedDebitedAmount(updatedRecurringPayment.getCumulatedDebitedAmount) + .withCumulatedFeesAmount(updatedRecurringPayment.getCumulatedFeesAmount) + .withLastUpdated(lastUpdated) + .copy(nextRecurringPaymentDate = + updatedRecurringPayment.nextRecurringPaymentDate + ) + } + ) :+ { + updatedRecurringPayment.nextRecurringPaymentDate match { + case Some(value) => + ExternalEntityToSchedulerEvent( + ExternalEntityToSchedulerEvent.Wrapped.AddSchedule( + AddSchedule( + Schedule( + persistenceId, + entityId, + s"$nextRecurringPayment#${recurringPayment.getId}", + 1, + Some(false), + Some(value), + None + ) + ) + ) + ) + case _ => + ExternalEntityToSchedulerEvent( + ExternalEntityToSchedulerEvent.Wrapped.RemoveSchedule( + RemoveSchedule( + persistenceId, + entityId, + s"$nextRecurringPayment#${recurringPayment.getId}" + ) + ) + ) + } + } :+ + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => + (if (first) FirstRecurringPaidIn(transaction.id, transaction.status) + else NextRecurringPaid(transaction.id, transaction.status)) ~> replyTo + ) + } else { + log.error( + "RecurringPayment-{} failed: {} -> {}", + recurringPayment.getId, + transaction.id, + asJson(transaction) + ) + Effect + .persist( + List( + if (first) { + FirstRecurringCardPaymentFailedEvent.defaultInstance + .withDebitedAccount(paymentAccount.externalUuid) + .withResultMessage(transaction.getReasonMessage) + .withDebitedAmount(transaction.amount) + .withFeesAmount(transaction.fees) + .withCurrency(transaction.currency) + .withTransaction(transaction) + .withRecurringPaymentRegistrationId(recurringPayment.getId) + .withFrequency(recurringPayment.getFrequency) + } else { + NextRecurringPaymentFailedEvent.defaultInstance + .withDebitedAccount(paymentAccount.externalUuid) + .withResultMessage(transaction.getReasonMessage) + .withDebitedAmount(transaction.amount) + .withFeesAmount(transaction.fees) + .withCurrency(transaction.currency) + .withTransaction(transaction) + .withRecurringPaymentRegistrationId(recurringPayment.getId) + .withType(recurringPayment.`type`) + .withFrequency(recurringPayment.getFrequency) + .withNumberOfRecurringPayments(recurringPayment.getNumberOfRecurringPayments) + .copy(lastRecurringPaymentDate = recurringPayment.lastRecurringPaymentDate) + } + ) :+ { + recurringPayment.nextRecurringPaymentDate match { + case Some(value) => + ExternalEntityToSchedulerEvent( + ExternalEntityToSchedulerEvent.Wrapped.AddSchedule( + AddSchedule( + Schedule( + persistenceId, + entityId, + s"$nextRecurringPayment#${recurringPayment.getId}", + 1, + Some(false), + Some(value), + None + ) + ) + ) + ) + case _ => + ExternalEntityToSchedulerEvent( + ExternalEntityToSchedulerEvent.Wrapped.RemoveSchedule( + RemoveSchedule( + persistenceId, + entityId, + s"$nextRecurringPayment#${recurringPayment.getId}" + ) + ) + ) + } + } :+ + PaymentAccountUpsertedEvent.defaultInstance + .withDocument(updatedPaymentAccount) + .withLastUpdated(lastUpdated) + ) + .thenRun(_ => + ( + if (first) + FirstRecurringCardPaymentFailed( + transaction.id, + transaction.status, + transaction.getReasonMessage + ) + else + NextRecurringPaymentFailed( + transaction.id, + transaction.status, + transaction.getReasonMessage + ) + ) ~> replyTo + ) + } + } + } + + private[this] def handleNextRecurringPaymentFailure( + entityId: String, + replyTo: Option[ActorRef[PaymentResult]], + paymentAccount: PaymentAccount, + recurringPayment: RecurringPayment, + debitedAmount: Int, + feesAmount: Int, + currency: String, + reason: String + )(implicit context: ActorContext[_]): Effect[ExternalSchedulerEvent, Option[PaymentAccount]] = { + Effect + .persist( + List( + NextRecurringPaymentFailedEvent.defaultInstance + .withDebitedAccount(paymentAccount.externalUuid) + .withResultMessage(reason) + .withDebitedAmount(debitedAmount) + .withFeesAmount(feesAmount) + .withCurrency(currency) + .withRecurringPaymentRegistrationId(recurringPayment.getId) + .withType(recurringPayment.`type`) + .withFrequency(recurringPayment.getFrequency) + .withNumberOfRecurringPayments(recurringPayment.getNumberOfRecurringPayments) + .copy(lastRecurringPaymentDate = recurringPayment.lastRecurringPaymentDate) + ) :+ { + recurringPayment.nextRecurringPaymentDate match { + case Some(value) => + ExternalEntityToSchedulerEvent( + ExternalEntityToSchedulerEvent.Wrapped.AddSchedule( + AddSchedule( + Schedule( + persistenceId, + entityId, + s"$nextRecurringPayment#${recurringPayment.getId}", + 1, + Some(false), + Some(value), + None + ) + ) + ) + ) + case _ => + ExternalEntityToSchedulerEvent( + ExternalEntityToSchedulerEvent.Wrapped.RemoveSchedule( + RemoveSchedule( + persistenceId, + entityId, + s"$nextRecurringPayment#${recurringPayment.getId}" + ) + ) + ) + } + } + ) + .thenRun(_ => + NextRecurringPaymentFailed( + "", + Transaction.TransactionStatus.TRANSACTION_NOT_SPECIFIED, + reason + ) ~> replyTo + ) + } + +} 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 e389e87..9ed6299 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/CardPaymentEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/CardPaymentEndpoints.scala @@ -274,7 +274,7 @@ trait CardPaymentEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { val browserInfo = extractBrowserInfo(language, accept, userAgent, payment) import payment._ run( - PayInFirstRecurring( + ExecuteFirstRecurringPayment( recurringPaymentRegistrationId, externalUuidWithProfile(principal._2), if (browserInfo.isDefined) Some(ipAddress) else None, diff --git a/core/src/main/scala/app/softnetwork/payment/service/PaymentService.scala b/core/src/main/scala/app/softnetwork/payment/service/PaymentService.scala index 464d411..9c3cbb0 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/PaymentService.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/PaymentService.scala @@ -275,7 +275,7 @@ trait PaymentService[SD <: SessionData with SessionDataDecorator[SD]] } ~ pathPrefix(recurringPaymentRoute) { pathPrefix(Segment) { recurringPaymentRegistrationId => run( - PayInFirstRecurring( + ExecuteFirstRecurringPayment( recurringPaymentRegistrationId, externalUuidWithProfile(session), if (browserInfo.isDefined) Some(ipAddress) else None, 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 d314e72..61fb247 100644 --- a/mangopay/src/main/scala/app/softnetwork/payment/spi/MangoPayProvider.scala +++ b/mangopay/src/main/scala/app/softnetwork/payment/spi/MangoPayProvider.scala @@ -2472,7 +2472,7 @@ trait MangoPayProvider extends PaymentProvider { case Some(firstRecurringPaymentTransaction) => import recurringPaymentTransaction._ val recurringPayInCIT: RecurringPayInCIT = new RecurringPayInCIT - recurringPayInCIT.setRecurringPayInRegistrationId(recurringPayInRegistrationId) + recurringPayInCIT.setRecurringPayInRegistrationId(recurringPaymentRegistrationId) firstRecurringPaymentTransaction.ipAddress match { case Some(ipAddress) => recurringPayInCIT.setIpAddress(ipAddress) case _ => @@ -2504,7 +2504,7 @@ trait MangoPayProvider extends PaymentProvider { recurringPayInCIT.setTag(externalUuid) recurringPayInCIT.setStatementDescriptor(statementDescriptor) recurringPayInCIT.setSecureModeReturnURL( - s"${config.recurringPaymentReturnUrl}/$recurringPayInRegistrationId" + s"${config.recurringPaymentReturnUrl}/$recurringPaymentRegistrationId" ) Try( MangoPay().getPayInApi.createRecurringPayInCIT(generateUUID(), recurringPayInCIT) @@ -2536,7 +2536,7 @@ trait MangoPayProvider extends PaymentProvider { ), authorId = result.getAuthorId, creditedWalletId = Option(result.getCreditedWalletId), - recurringPayInRegistrationId = Option(recurringPayInRegistrationId) + recurringPayInRegistrationId = Option(recurringPaymentRegistrationId) ) ) case Failure(f) => @@ -2546,7 +2546,7 @@ trait MangoPayProvider extends PaymentProvider { case _ => import recurringPaymentTransaction._ val recurringPayInMIT: RecurringPayInMIT = new RecurringPayInMIT - recurringPayInMIT.setRecurringPayInRegistrationId(recurringPayInRegistrationId) + recurringPayInMIT.setRecurringPayInRegistrationId(recurringPaymentRegistrationId) recurringPayInMIT.setStatementDescriptor(statementDescriptor) val debitedFunds = new Money debitedFunds.setAmount(debitedAmount) @@ -2582,7 +2582,7 @@ trait MangoPayProvider extends PaymentProvider { resultMessage = Option(result.getResultMessage).getOrElse(""), authorId = result.getAuthorId, creditedWalletId = Option(result.getCreditedWalletId), - recurringPayInRegistrationId = Option(recurringPayInRegistrationId) + recurringPayInRegistrationId = Option(recurringPaymentRegistrationId) ) ) case Failure(f) => 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 b30f36e..de7ddcd 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/spi/MockMangoPayProvider.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/spi/MockMangoPayProvider.scala @@ -1538,7 +1538,7 @@ trait MockMangoPayProvider extends MangoPayProvider { recurringPaymentTransaction: RecurringPaymentTransaction ): Option[Transaction] = { RecurringCardPaymentRegistrations.get( - recurringPaymentTransaction.recurringPayInRegistrationId + recurringPaymentTransaction.recurringPaymentRegistrationId ) match { case Some(recurringPaymentRegistration) => recurringPaymentTransaction.extension[Option[FirstRecurringPaymentTransaction]]( @@ -1547,7 +1547,7 @@ trait MockMangoPayProvider extends MangoPayProvider { case Some(firstRecurringPaymentTransaction) => import recurringPaymentTransaction._ val recurringPayInCIT: RecurringPayInCIT = new RecurringPayInCIT - recurringPayInCIT.setRecurringPayInRegistrationId(recurringPayInRegistrationId) + recurringPayInCIT.setRecurringPayInRegistrationId(recurringPaymentRegistrationId) recurringPayInCIT.setIpAddress(firstRecurringPaymentTransaction.ipAddress.getOrElse("")) val debitedFunds = new Money debitedFunds.setAmount(debitedAmount) @@ -1560,7 +1560,7 @@ trait MockMangoPayProvider extends MangoPayProvider { recurringPayInCIT.setTag(externalUuid) recurringPayInCIT.setStatementDescriptor(statementDescriptor) recurringPayInCIT.setSecureModeReturnURL( - s"${config.recurringPaymentReturnUrl}/$recurringPayInRegistrationId" + s"${config.recurringPaymentReturnUrl}/$recurringPaymentRegistrationId" ) import recurringPaymentRegistration._ @@ -1595,7 +1595,7 @@ trait MockMangoPayProvider extends MangoPayProvider { val previousState = recurringPaymentRegistration.currentState RecurringCardPaymentRegistrations = RecurringCardPaymentRegistrations.updated( - recurringPayInRegistrationId, + recurringPaymentRegistrationId, recurringPaymentRegistration.copy( status = "pending", currentState = RecurringCardPaymentState( @@ -1624,13 +1624,13 @@ trait MockMangoPayProvider extends MangoPayProvider { else None, authorId = registration.getAuthorId, creditedWalletId = Some(registration.getCreditedWalletId), - recurringPayInRegistrationId = Option(recurringPayInRegistrationId) + recurringPayInRegistrationId = Option(recurringPaymentRegistrationId) ) ) case _ => import recurringPaymentTransaction._ val recurringPayInMIT: RecurringPayInMIT = new RecurringPayInMIT - recurringPayInMIT.setRecurringPayInRegistrationId(recurringPayInRegistrationId) + recurringPayInMIT.setRecurringPayInRegistrationId(recurringPaymentRegistrationId) recurringPayInMIT.setStatementDescriptor(statementDescriptor) val debitedFunds = new Money debitedFunds.setAmount(debitedAmount) @@ -1671,7 +1671,7 @@ trait MockMangoPayProvider extends MangoPayProvider { val previousState = recurringPaymentRegistration.currentState RecurringCardPaymentRegistrations = RecurringCardPaymentRegistrations.updated( - recurringPayInRegistrationId, + recurringPaymentRegistrationId, recurringPaymentRegistration.copy( status = "pending", currentState = RecurringCardPaymentState( @@ -1698,7 +1698,7 @@ trait MockMangoPayProvider extends MangoPayProvider { redirectUrl = None, authorId = registration.getAuthorId, creditedWalletId = Some(registration.getCreditedWalletId), - recurringPayInRegistrationId = Option(recurringPayInRegistrationId) + recurringPayInRegistrationId = Option(recurringPaymentRegistrationId) ) ) } 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 1f78915..e5ff3b8 100644 --- a/testkit/src/test/scala/app/softnetwork/payment/handlers/PaymentHandlerSpec.scala +++ b/testkit/src/test/scala/app/softnetwork/payment/handlers/PaymentHandlerSpec.scala @@ -1263,7 +1263,7 @@ class PaymentHandlerSpec "execute first recurring card payment" in { !?( - PayInFirstRecurring( + ExecuteFirstRecurringPayment( recurringPaymentRegistrationId, computeExternalUuidWithProfile(customerUuid, Some("customer")) )