diff --git a/api/src/main/scala/app/softnetwork/payment/api/SoftPayApi.scala b/api/src/main/scala/app/softnetwork/payment/api/SoftPayApi.scala index 7a5ec4f..34c0b27 100644 --- a/api/src/main/scala/app/softnetwork/payment/api/SoftPayApi.scala +++ b/api/src/main/scala/app/softnetwork/payment/api/SoftPayApi.scala @@ -70,14 +70,17 @@ trait SoftPayApi[SD <: SessionData with SessionDataDecorator[SD]] extends SoftPa override implicit def system: ActorSystem[_] = sys } - def paymentAccountToJdbcProcessorStream: ActorSystem[_] => PaymentAccountToJdbcProcessStream + def paymentAccountToJdbcProcessorStream + : ActorSystem[_] => Option[PaymentAccountToJdbcProcessStream] = + _ => None def transactionToJdbcProcessorStream: ActorSystem[_] => TransactionToJdbcProcessorStream - override def paymentEventProcessorStreams: ActorSystem[_] => Seq[EventProcessorStream[_]] = sys => - super.paymentEventProcessorStreams(sys) :+ paymentAccountToJdbcProcessorStream( - sys - ) :+ transactionToJdbcProcessorStream(sys) + override def paymentEventProcessorStreams: ActorSystem[_] => Seq[EventProcessorStream[_]] = + sys => + super.paymentEventProcessorStreams(sys) ++ + paymentAccountToJdbcProcessorStream(sys).toSeq :+ + transactionToJdbcProcessorStream(sys) implicit def sessionConfig: SessionConfig = Settings.Session.DefaultSessionConfig diff --git a/api/src/main/scala/app/softnetwork/payment/api/SoftPayEndpointsPostgresLauncher.scala b/api/src/main/scala/app/softnetwork/payment/api/SoftPayEndpointsPostgresLauncher.scala index f69eb57..b826623 100644 --- a/api/src/main/scala/app/softnetwork/payment/api/SoftPayEndpointsPostgresLauncher.scala +++ b/api/src/main/scala/app/softnetwork/payment/api/SoftPayEndpointsPostgresLauncher.scala @@ -34,15 +34,16 @@ object SoftPayEndpointsPostgresLauncher override protected def manager: SessionManager[JwtClaims] = SessionManagers.jwt override def paymentAccountToJdbcProcessorStream - : ActorSystem[_] => PaymentAccountToJdbcProcessStream = sys => - new PaymentAccountToJdbcProcessStream - with JdbcJournalProvider - with JdbcOffsetProvider - with PostgresProfile { - override implicit def system: ActorSystem[_] = sys - - override lazy val config: Config = SoftPayEndpointsPostgresLauncher.this.config - } + : ActorSystem[_] => Option[PaymentAccountToJdbcProcessStream] = sys => + Some( + new PaymentAccountToJdbcProcessStream + with JdbcJournalProvider + with JdbcOffsetProvider + with PostgresProfile { + override implicit def system: ActorSystem[_] = sys + override lazy val config: Config = SoftPayEndpointsPostgresLauncher.this.config + } + ) override def transactionToJdbcProcessorStream : ActorSystem[_] => TransactionToJdbcProcessorStream = sys => @@ -51,7 +52,6 @@ object SoftPayEndpointsPostgresLauncher with JdbcOffsetProvider with PostgresProfile { override implicit def system: ActorSystem[_] = sys - override lazy val config: Config = SoftPayEndpointsPostgresLauncher.this.config } } diff --git a/api/src/main/scala/app/softnetwork/payment/api/SoftPayRoutesPostgresLauncher.scala b/api/src/main/scala/app/softnetwork/payment/api/SoftPayRoutesPostgresLauncher.scala index 15b9a1a..359b9cc 100644 --- a/api/src/main/scala/app/softnetwork/payment/api/SoftPayRoutesPostgresLauncher.scala +++ b/api/src/main/scala/app/softnetwork/payment/api/SoftPayRoutesPostgresLauncher.scala @@ -33,15 +33,16 @@ object SoftPayRoutesPostgresLauncher override protected def manager: SessionManager[JwtClaims] = SessionManagers.jwt override def paymentAccountToJdbcProcessorStream - : ActorSystem[_] => PaymentAccountToJdbcProcessStream = sys => - new PaymentAccountToJdbcProcessStream - with JdbcJournalProvider - with JdbcOffsetProvider - with PostgresProfile { - override implicit def system: ActorSystem[_] = sys - - override lazy val config: Config = SoftPayRoutesPostgresLauncher.this.config - } + : ActorSystem[_] => Option[PaymentAccountToJdbcProcessStream] = sys => + Some( + new PaymentAccountToJdbcProcessStream + with JdbcJournalProvider + with JdbcOffsetProvider + with PostgresProfile { + override implicit def system: ActorSystem[_] = sys + override lazy val config: Config = SoftPayRoutesPostgresLauncher.this.config + } + ) override def transactionToJdbcProcessorStream : ActorSystem[_] => TransactionToJdbcProcessorStream = sys => diff --git a/api/src/main/scala/app/softnetwork/payment/api/SoftPayWithSchedulerEndpointsPostgresLauncher.scala b/api/src/main/scala/app/softnetwork/payment/api/SoftPayWithSchedulerEndpointsPostgresLauncher.scala index e43b0d8..fbe7a64 100644 --- a/api/src/main/scala/app/softnetwork/payment/api/SoftPayWithSchedulerEndpointsPostgresLauncher.scala +++ b/api/src/main/scala/app/softnetwork/payment/api/SoftPayWithSchedulerEndpointsPostgresLauncher.scala @@ -34,15 +34,17 @@ object SoftPayWithSchedulerEndpointsPostgresLauncher override protected def manager: SessionManager[JwtClaims] = SessionManagers.jwt override def paymentAccountToJdbcProcessorStream - : ActorSystem[_] => PaymentAccountToJdbcProcessStream = sys => - new PaymentAccountToJdbcProcessStream - with JdbcJournalProvider - with JdbcOffsetProvider - with PostgresProfile { - override implicit def system: ActorSystem[_] = sys - - override lazy val config: Config = SoftPayWithSchedulerEndpointsPostgresLauncher.this.config - } + : ActorSystem[_] => Option[PaymentAccountToJdbcProcessStream] = sys => + Some( + new PaymentAccountToJdbcProcessStream + with JdbcJournalProvider + with JdbcOffsetProvider + with PostgresProfile { + override implicit def system: ActorSystem[_] = sys + override lazy val config: Config = + SoftPayWithSchedulerEndpointsPostgresLauncher.this.config + } + ) override def transactionToJdbcProcessorStream : ActorSystem[_] => TransactionToJdbcProcessorStream = sys => diff --git a/api/src/main/scala/app/softnetwork/payment/api/SoftPayWithSchedulerRoutesPostgresLauncher.scala b/api/src/main/scala/app/softnetwork/payment/api/SoftPayWithSchedulerRoutesPostgresLauncher.scala index fde159a..19dbc4b 100644 --- a/api/src/main/scala/app/softnetwork/payment/api/SoftPayWithSchedulerRoutesPostgresLauncher.scala +++ b/api/src/main/scala/app/softnetwork/payment/api/SoftPayWithSchedulerRoutesPostgresLauncher.scala @@ -31,15 +31,16 @@ object SoftPayWithSchedulerRoutesPostgresLauncher override protected def manager: SessionManager[JwtClaims] = SessionManagers.jwt override def paymentAccountToJdbcProcessorStream - : ActorSystem[_] => PaymentAccountToJdbcProcessStream = sys => - new PaymentAccountToJdbcProcessStream - with JdbcJournalProvider - with JdbcOffsetProvider - with PostgresProfile { - override implicit def system: ActorSystem[_] = sys - - override lazy val config: Config = SoftPayWithSchedulerRoutesPostgresLauncher.this.config - } + : ActorSystem[_] => Option[PaymentAccountToJdbcProcessStream] = sys => + Some( + new PaymentAccountToJdbcProcessStream + with JdbcJournalProvider + with JdbcOffsetProvider + with PostgresProfile { + override implicit def system: ActorSystem[_] = sys + override lazy val config: Config = SoftPayWithSchedulerRoutesPostgresLauncher.this.config + } + ) override def transactionToJdbcProcessorStream : ActorSystem[_] => TransactionToJdbcProcessorStream = sys => diff --git a/build.sbt b/build.sbt index 3f89694..3c365a3 100644 --- a/build.sbt +++ b/build.sbt @@ -18,7 +18,7 @@ ThisBuild / organization := "app.softnetwork" name := "payment" -ThisBuild / version := "0.9.2" +ThisBuild / version := "0.9.3" ThisBuild / scalaVersion := scala212 diff --git a/core/src/main/resources/db/migration/transaction/V1__create_transaction.sql b/core/src/main/resources/db/migration/transaction/V1__create_transaction.sql new file mode 100644 index 0000000..b387493 --- /dev/null +++ b/core/src/main/resources/db/migration/transaction/V1__create_transaction.sql @@ -0,0 +1,52 @@ +CREATE TABLE IF NOT EXISTS transaction ( + id VARCHAR(255) NOT NULL PRIMARY KEY, + order_uuid VARCHAR(255) NOT NULL, + nature VARCHAR(20) NOT NULL, + transaction_type VARCHAR(30) NOT NULL, + status VARCHAR(50) NOT NULL, + amount INTEGER NOT NULL, + currency VARCHAR(3) NOT NULL DEFAULT 'EUR', + fees INTEGER NOT NULL DEFAULT 0, + result_code VARCHAR(50) NOT NULL, + result_message TEXT NOT NULL, + author_id VARCHAR(255) NOT NULL, + payment_type VARCHAR(20) NOT NULL DEFAULT 'CARD', + created_date TIMESTAMP NOT NULL, + last_updated TIMESTAMP NOT NULL, + payment_method_id VARCHAR(255), + redirect_url TEXT, + reason_message TEXT, + credited_wallet_id VARCHAR(255), + credited_user_id VARCHAR(255), + debited_wallet_id VARCHAR(255), + debited_user_id VARCHAR(255), + mandate_id VARCHAR(255), + pre_authorization_id VARCHAR(255), + recurring_pay_in_registration_id VARCHAR(255), + external_reference VARCHAR(255), + pre_authorization_validated BOOLEAN, + pre_authorization_canceled BOOLEAN, + pre_authorization_expired BOOLEAN, + pre_authorization_debited_amount INTEGER, + return_url TEXT, + paypal_payer_email VARCHAR(255), + idempotency_key VARCHAR(255), + client_id VARCHAR(255), + payment_client_secret TEXT, + payment_client_data TEXT, + payment_client_return_url TEXT, + source_transaction_id VARCHAR(255), + transfer_amount INTEGER, + pre_registration_id VARCHAR(255), + paypal_payer_id VARCHAR(255), + pay_in_id VARCHAR(255), + deleted BOOLEAN NOT NULL DEFAULT FALSE +); + +CREATE INDEX idx_transaction_order_uuid ON transaction(order_uuid); +CREATE INDEX idx_transaction_author_id ON transaction(author_id); +CREATE INDEX idx_transaction_status ON transaction(status); +CREATE INDEX idx_transaction_type ON transaction(transaction_type); +CREATE INDEX idx_transaction_client_id ON transaction(client_id); +CREATE INDEX idx_transaction_recurring ON transaction(recurring_pay_in_registration_id); +CREATE INDEX idx_transaction_payment_type ON transaction(payment_type); diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/query/JdbcTransactionProvider.scala b/core/src/main/scala/app/softnetwork/payment/persistence/query/JdbcTransactionProvider.scala index 3a29088..619d72e 100644 --- a/core/src/main/scala/app/softnetwork/payment/persistence/query/JdbcTransactionProvider.scala +++ b/core/src/main/scala/app/softnetwork/payment/persistence/query/JdbcTransactionProvider.scala @@ -1,25 +1,431 @@ package app.softnetwork.payment.persistence.query import app.softnetwork.payment.model.Transaction -import app.softnetwork.payment.serialization.paymentFormats -import app.softnetwork.persistence.jdbc.query.JdbcStateProvider -import app.softnetwork.persistence.model.StateWrappertReader import app.softnetwork.persistence.ManifestWrapper -import org.json4s.Formats -import slick.jdbc.JdbcProfile +import app.softnetwork.persistence.jdbc.query.ColumnMappedJdbcStateProvider + +import slick.jdbc.{GetResult, JdbcProfile} trait JdbcTransactionProvider - extends JdbcStateProvider[Transaction] + extends ColumnMappedJdbcStateProvider[Transaction] with ManifestWrapper[Transaction] { _: JdbcProfile => - override implicit def formats: Formats = paymentFormats + import api._ override protected val manifestWrapper: ManifestW = ManifestW() - override def reader: StateWrappertReader[Transaction] = - new StateWrappertReader[Transaction] { - override protected val manifestWrapper: ManifestW = ManifestW() - } + protected implicit val getInstantResult: GetResult[java.time.Instant] = + GetResult(r => r.nextTimestamp().toInstant) + + protected implicit val getOptInstantResult: GetResult[Option[java.time.Instant]] = + GetResult(r => Option(r.nextTimestampOption()).flatten.map(_.toInstant)) + + // All 41 proto fields + deleted flag = 42 columns + case class TransactionRow( + // required fields + id: String, + orderUuid: String, + nature: String, + transactionType: String, + status: String, + amount: Int, + currency: String, + fees: Int, + resultCode: String, + resultMessage: String, + authorId: String, + paymentType: String, + createdDate: java.time.Instant, + lastUpdated: java.time.Instant, + // optional fields + paymentMethodId: Option[String], + redirectUrl: Option[String], + reasonMessage: Option[String], + creditedWalletId: Option[String], + creditedUserId: Option[String], + debitedWalletId: Option[String], + debitedUserId: Option[String], + mandateId: Option[String], + preAuthorizationId: Option[String], + recurringPayInRegistrationId: Option[String], + externalReference: Option[String], + preAuthorizationValidated: Option[Boolean], + preAuthorizationCanceled: Option[Boolean], + preAuthorizationExpired: Option[Boolean], + preAuthorizationDebitedAmount: Option[Int], + returnUrl: Option[String], + payPalPayerEmail: Option[String], + idempotencyKey: Option[String], + clientId: Option[String], + paymentClientSecret: Option[String], + paymentClientData: Option[String], + paymentClientReturnUrl: Option[String], + sourceTransactionId: Option[String], + transferAmount: Option[Int], + preRegistrationId: Option[String], + payPalPayerId: Option[String], + payInId: Option[String], + // soft delete + deleted: Boolean + ) + + type RowType = TransactionRow + + class Transactions(tag: Tag) extends Table[RowType](tag, dataset, tableName) { + def id = column[String]("id", O.PrimaryKey) + def orderUuid = column[String]("order_uuid") + def nature = column[String]("nature") + def transactionType = column[String]("transaction_type") + def status = column[String]("status") + def amount = column[Int]("amount") + def currency = column[String]("currency") + def fees = column[Int]("fees") + def resultCode = column[String]("result_code") + def resultMessage = column[String]("result_message") + def authorId = column[String]("author_id") + def paymentType = column[String]("payment_type") + def createdDate = column[java.time.Instant]("created_date") + def lastUpdated = column[java.time.Instant]("last_updated") + def paymentMethodId = column[Option[String]]("payment_method_id") + def redirectUrl = column[Option[String]]("redirect_url") + def reasonMessage = column[Option[String]]("reason_message") + def creditedWalletId = column[Option[String]]("credited_wallet_id") + def creditedUserId = column[Option[String]]("credited_user_id") + def debitedWalletId = column[Option[String]]("debited_wallet_id") + def debitedUserId = column[Option[String]]("debited_user_id") + def mandateId = column[Option[String]]("mandate_id") + def preAuthorizationId = column[Option[String]]("pre_authorization_id") + def recurringPayInRegistrationId = column[Option[String]]("recurring_pay_in_registration_id") + def externalReference = column[Option[String]]("external_reference") + def preAuthorizationValidated = column[Option[Boolean]]("pre_authorization_validated") + def preAuthorizationCanceled = column[Option[Boolean]]("pre_authorization_canceled") + def preAuthorizationExpired = column[Option[Boolean]]("pre_authorization_expired") + def preAuthorizationDebitedAmount = column[Option[Int]]("pre_authorization_debited_amount") + def returnUrl = column[Option[String]]("return_url") + def payPalPayerEmail = column[Option[String]]("paypal_payer_email") + def idempotencyKey = column[Option[String]]("idempotency_key") + def clientId = column[Option[String]]("client_id") + def paymentClientSecret = column[Option[String]]("payment_client_secret") + def paymentClientData = column[Option[String]]("payment_client_data") + def paymentClientReturnUrl = column[Option[String]]("payment_client_return_url") + def sourceTransactionId = column[Option[String]]("source_transaction_id") + def transferAmount = column[Option[Int]]("transfer_amount") + def preRegistrationId = column[Option[String]]("pre_registration_id") + def payPalPayerId = column[Option[String]]("paypal_payer_id") + def payInId = column[Option[String]]("pay_in_id") + def deleted = column[Boolean]("deleted") + + // Split into 4 parts to stay within Scala 2's 22-element tuple limit (42 columns) + private val part1 = ( + id, + orderUuid, + nature, + transactionType, + status, + amount, + currency, + fees, + resultCode, + resultMessage, + authorId + ) + private val part2 = ( + paymentType, + createdDate, + lastUpdated, + paymentMethodId, + redirectUrl, + reasonMessage, + creditedWalletId, + creditedUserId, + debitedWalletId, + debitedUserId, + mandateId + ) + private val part3 = ( + preAuthorizationId, + recurringPayInRegistrationId, + externalReference, + preAuthorizationValidated, + preAuthorizationCanceled, + preAuthorizationExpired, + preAuthorizationDebitedAmount, + returnUrl, + payPalPayerEmail, + idempotencyKey, + clientId + ) + private val part4 = ( + paymentClientSecret, + paymentClientData, + paymentClientReturnUrl, + sourceTransactionId, + transferAmount, + preRegistrationId, + payPalPayerId, + payInId, + deleted + ) + def * = (part1, part2, part3, part4) <> ({ case (p1, p2, p3, p4) => + TransactionRow( + p1._1, + p1._2, + p1._3, + p1._4, + p1._5, + p1._6, + p1._7, + p1._8, + p1._9, + p1._10, + p1._11, + p2._1, + p2._2, + p2._3, + p2._4, + p2._5, + p2._6, + p2._7, + p2._8, + p2._9, + p2._10, + p2._11, + p3._1, + p3._2, + p3._3, + p3._4, + p3._5, + p3._6, + p3._7, + p3._8, + p3._9, + p3._10, + p3._11, + p4._1, + p4._2, + p4._3, + p4._4, + p4._5, + p4._6, + p4._7, + p4._8, + p4._9 + ) + }, { (r: TransactionRow) => + Some( + ( + ( + r.id, + r.orderUuid, + r.nature, + r.transactionType, + r.status, + r.amount, + r.currency, + r.fees, + r.resultCode, + r.resultMessage, + r.authorId + ), + ( + r.paymentType, + r.createdDate, + r.lastUpdated, + r.paymentMethodId, + r.redirectUrl, + r.reasonMessage, + r.creditedWalletId, + r.creditedUserId, + r.debitedWalletId, + r.debitedUserId, + r.mandateId + ), + ( + r.preAuthorizationId, + r.recurringPayInRegistrationId, + r.externalReference, + r.preAuthorizationValidated, + r.preAuthorizationCanceled, + r.preAuthorizationExpired, + r.preAuthorizationDebitedAmount, + r.returnUrl, + r.payPalPayerEmail, + r.idempotencyKey, + r.clientId + ), + ( + r.paymentClientSecret, + r.paymentClientData, + r.paymentClientReturnUrl, + r.sourceTransactionId, + r.transferAmount, + r.preRegistrationId, + r.payPalPayerId, + r.payInId, + r.deleted + ) + ) + ) + }) + } + + type TableType = Transactions + override def tableQuery: TableQuery[Transactions] = TableQuery[Transactions] + + override def rowUuidColumn(row: Transactions): Rep[String] = row.id + + /** Compatibility method for tests that use the old JdbcStateProvider.load(uuid) API */ + def load(uuid: String): Option[Transaction] = loadDocument(uuid) + + override def toRow(entity: Transaction, deleted: Boolean = false): RowType = + TransactionRow( + id = entity.id, + orderUuid = entity.orderUuid, + nature = entity.nature.name, + transactionType = entity.`type`.name, + status = entity.status.name, + amount = entity.amount, + currency = entity.currency, + fees = entity.fees, + resultCode = entity.resultCode, + resultMessage = entity.resultMessage, + authorId = entity.authorId, + paymentType = entity.paymentType.name, + createdDate = entity.createdDate, + lastUpdated = entity.lastUpdated, + paymentMethodId = entity.paymentMethodId, + redirectUrl = entity.redirectUrl, + reasonMessage = entity.reasonMessage, + creditedWalletId = entity.creditedWalletId, + creditedUserId = entity.creditedUserId, + debitedWalletId = entity.debitedWalletId, + debitedUserId = entity.debitedUserId, + mandateId = entity.mandateId, + preAuthorizationId = entity.preAuthorizationId, + recurringPayInRegistrationId = entity.recurringPayInRegistrationId, + externalReference = entity.externalReference, + preAuthorizationValidated = entity.preAuthorizationValidated, + preAuthorizationCanceled = entity.preAuthorizationCanceled, + preAuthorizationExpired = entity.preAuthorizationExpired, + preAuthorizationDebitedAmount = entity.preAuthorizationDebitedAmount, + returnUrl = entity.returnUrl, + payPalPayerEmail = entity.payPalPayerEmail, + idempotencyKey = entity.idempotencyKey, + clientId = entity.clientId, + paymentClientSecret = entity.paymentClientSecret, + paymentClientData = entity.paymentClientData, + paymentClientReturnUrl = entity.paymentClientReturnUrl, + sourceTransactionId = entity.sourceTransactionId, + transferAmount = entity.transferAmount, + preRegistrationId = entity.preRegistrationId, + payPalPayerId = entity.payPalPayerId, + payInId = entity.payInId, + deleted = deleted + ) + + override def fromRow(row: RowType): Option[Transaction] = { + if (row.deleted) None + else + Some( + Transaction( + id = row.id, + orderUuid = row.orderUuid, + nature = Transaction.TransactionNature + .fromName(row.nature) + .getOrElse(Transaction.TransactionNature.REGULAR), + `type` = Transaction.TransactionType + .fromName(row.transactionType) + .getOrElse(Transaction.TransactionType.PAYIN), + status = Transaction.TransactionStatus + .fromName(row.status) + .getOrElse(Transaction.TransactionStatus.TRANSACTION_CREATED), + amount = row.amount, + currency = row.currency, + fees = row.fees, + resultCode = row.resultCode, + resultMessage = row.resultMessage, + authorId = row.authorId, + paymentType = Transaction.PaymentType + .fromName(row.paymentType) + .getOrElse(Transaction.PaymentType.CARD), + createdDate = row.createdDate, + lastUpdated = row.lastUpdated, + paymentMethodId = row.paymentMethodId, + redirectUrl = row.redirectUrl, + reasonMessage = row.reasonMessage, + creditedWalletId = row.creditedWalletId, + creditedUserId = row.creditedUserId, + debitedWalletId = row.debitedWalletId, + debitedUserId = row.debitedUserId, + mandateId = row.mandateId, + preAuthorizationId = row.preAuthorizationId, + recurringPayInRegistrationId = row.recurringPayInRegistrationId, + externalReference = row.externalReference, + preAuthorizationValidated = row.preAuthorizationValidated, + preAuthorizationCanceled = row.preAuthorizationCanceled, + preAuthorizationExpired = row.preAuthorizationExpired, + preAuthorizationDebitedAmount = row.preAuthorizationDebitedAmount, + returnUrl = row.returnUrl, + payPalPayerEmail = row.payPalPayerEmail, + idempotencyKey = row.idempotencyKey, + clientId = row.clientId, + paymentClientSecret = row.paymentClientSecret, + paymentClientData = row.paymentClientData, + paymentClientReturnUrl = row.paymentClientReturnUrl, + sourceTransactionId = row.sourceTransactionId, + transferAmount = row.transferAmount, + preRegistrationId = row.preRegistrationId, + payPalPayerId = row.payPalPayerId, + payInId = row.payInId + ) + ) + } + implicit val getResult: GetResult[RowType] = GetResult { r => + TransactionRow( + id = r.<<, + orderUuid = r.<<, + nature = r.<<, + transactionType = r.<<, + status = r.<<, + amount = r.<<, + currency = r.<<, + fees = r.<<, + resultCode = r.<<, + resultMessage = r.<<, + authorId = r.<<, + paymentType = r.<<, + createdDate = r.<<, + lastUpdated = r.<<, + paymentMethodId = r.< + override val externalProcessor: String = "jdbc" + override implicit lazy val executionContext: ExecutionContext = system.executionContext + override protected def init(): Unit = initTable() } diff --git a/project/Versions.scala b/project/Versions.scala index 548ee4a..1579d3e 100644 --- a/project/Versions.scala +++ b/project/Versions.scala @@ -1,6 +1,6 @@ object Versions { - val genericPersistence = "0.8.2" + val genericPersistence = "0.8.4" val scheduler = "0.8.0" diff --git a/stripe/src/main/scala/app/softnetwork/payment/service/StripeEventHandler.scala b/stripe/src/main/scala/app/softnetwork/payment/service/StripeEventHandler.scala index 1cd272f..38a55a0 100644 --- a/stripe/src/main/scala/app/softnetwork/payment/service/StripeEventHandler.scala +++ b/stripe/src/main/scala/app/softnetwork/payment/service/StripeEventHandler.scala @@ -16,8 +16,6 @@ import app.softnetwork.payment.model.{KycDocument, RecurringPayment} import com.stripe.model.{Account, Event, Invoice, Person, StripeObject, Subscription} import com.stripe.net.Webhook -import java.util.concurrent.ConcurrentHashMap - import scala.util.{Failure, Success, Try} import scala.jdk.CollectionConverters._ @@ -25,27 +23,26 @@ import scala.jdk.CollectionConverters._ */ trait StripeEventHandler extends Completion { _: BasicPaymentService with PaymentHandler => - /** Bounded set of recently processed webhook event IDs for idempotency. Stripe may deliver the - * same event multiple times. + /** Bounded LRU set of recently processed webhook event IDs for idempotency. Stripe may deliver + * the same event multiple times. Uses a synchronized LinkedHashMap configured as LRU to evict + * oldest entries first. */ - private[this] val processedEventIds: ConcurrentHashMap[String, java.lang.Boolean] = - new ConcurrentHashMap[String, java.lang.Boolean](256) - private[this] val MaxProcessedEventIds = 10000 + private[this] val processedEventIds: java.util.Map[String, java.lang.Boolean] = + java.util.Collections.synchronizedMap( + new java.util.LinkedHashMap[String, java.lang.Boolean](256, 0.75f, true) { + override def removeEldestEntry( + eldest: java.util.Map.Entry[String, java.lang.Boolean] + ): Boolean = + size() > MaxProcessedEventIds + } + ) + private[this] def isEventAlreadyProcessed(eventId: String): Boolean = { if (processedEventIds.containsKey(eventId)) { true } else { - if (processedEventIds.size() >= MaxProcessedEventIds) { - // Evict oldest entries (ConcurrentHashMap has no LRU, so clear half) - val iterator = processedEventIds.keys() - var count = 0 - while (iterator.hasMoreElements && count < MaxProcessedEventIds / 2) { - processedEventIds.remove(iterator.nextElement()) - count += 1 - } - } processedEventIds.put(eventId, java.lang.Boolean.TRUE) false } diff --git a/stripe/src/main/scala/app/softnetwork/payment/spi/StripeRecurringPaymentApi.scala b/stripe/src/main/scala/app/softnetwork/payment/spi/StripeRecurringPaymentApi.scala index e874318..99b4519 100644 --- a/stripe/src/main/scala/app/softnetwork/payment/spi/StripeRecurringPaymentApi.scala +++ b/stripe/src/main/scala/app/softnetwork/payment/spi/StripeRecurringPaymentApi.scala @@ -141,9 +141,18 @@ trait StripeRecurringPaymentApi extends RecurringPaymentApi { _: StripeContext = .setPriceData(priceDataBuilder.build()) .build() ) + .setAutomaticTax( + SubscriptionCreateParams.AutomaticTax + .builder() + .setEnabled(metadata.contains("automatic_tax")) + .build() + ) .putMetadata("transaction_type", "recurring_payment") .putMetadata("recurring_payment_type", "card") + // Propagate all user-supplied metadata to Stripe subscription + metadata.foreach { case (k, v) => params.putMetadata(k, v) } + recurringPayment.externalReference.foreach(ref => params.putMetadata("external_reference", ref) ) diff --git a/testkit/src/main/scala/app/softnetwork/payment/scalatest/JdbcPaymentTestKit.scala b/testkit/src/main/scala/app/softnetwork/payment/scalatest/JdbcPaymentTestKit.scala index 7703f33..6963ff5 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/scalatest/JdbcPaymentTestKit.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/scalatest/JdbcPaymentTestKit.scala @@ -26,18 +26,20 @@ trait JdbcPaymentTestKit extends PaymentTestKit with JdbcPersistenceTestKit { _: ) .withFallback(ConfigFactory.load()) - def jdbcPaymentAccountProvider: JdbcPaymentAccountProvider + def jdbcPaymentAccountProvider: Option[JdbcPaymentAccountProvider] = None def jdbcTransactionProvider: JdbcTransactionProvider - def paymentAccountToJdbcProcessorStream: ActorSystem[_] => PaymentAccountToJdbcProcessStream + def paymentAccountToJdbcProcessorStream + : ActorSystem[_] => Option[PaymentAccountToJdbcProcessStream] = _ => None def transactionToJdbcProcessorStream: ActorSystem[_] => TransactionToJdbcProcessorStream - override def paymentEventProcessorStreams: ActorSystem[_] => Seq[EventProcessorStream[_]] = sys => - super.paymentEventProcessorStreams(sys) :+ paymentAccountToJdbcProcessorStream( - sys - ) :+ transactionToJdbcProcessorStream(sys) + override def paymentEventProcessorStreams: ActorSystem[_] => Seq[EventProcessorStream[_]] = + sys => + super.paymentEventProcessorStreams(sys) ++ + paymentAccountToJdbcProcessorStream(sys).toSeq :+ + transactionToJdbcProcessorStream(sys) lazy val transactionProbe: TestProbe[TransactionUpdatedEvent] = createTestProbe[TransactionUpdatedEvent]() diff --git a/testkit/src/main/scala/app/softnetwork/payment/scalatest/PostgresPaymentTestKit.scala b/testkit/src/main/scala/app/softnetwork/payment/scalatest/PostgresPaymentTestKit.scala index 96ca222..60fde6d 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/scalatest/PostgresPaymentTestKit.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/scalatest/PostgresPaymentTestKit.scala @@ -20,36 +20,34 @@ import scala.concurrent.ExecutionContext trait PostgresPaymentTestKit extends JdbcPaymentTestKit with PostgresTestKit { _: Suite => - override lazy val jdbcPaymentAccountProvider: JdbcPaymentAccountProvider = - new JdbcPaymentAccountProvider with PostgresProfile { - override implicit def executionContext: ExecutionContext = classicSystem.dispatcher - - override implicit def classicSystem: actor.ActorSystem = typedSystem() - - override lazy val config: Config = PostgresPaymentTestKit.this.config - } + override lazy val jdbcPaymentAccountProvider: Option[JdbcPaymentAccountProvider] = + Some( + new JdbcPaymentAccountProvider with PostgresProfile { + override implicit def executionContext: ExecutionContext = classicSystem.dispatcher + override implicit def classicSystem: actor.ActorSystem = typedSystem() + override lazy val config: Config = PostgresPaymentTestKit.this.config + } + ) override lazy val jdbcTransactionProvider: JdbcTransactionProvider = new JdbcTransactionProvider with PostgresProfile { override implicit def executionContext: ExecutionContext = classicSystem.dispatcher - override implicit def classicSystem: actor.ActorSystem = typedSystem() - override lazy val config: Config = PostgresPaymentTestKit.this.config } override def paymentAccountToJdbcProcessorStream - : ActorSystem[_] => PaymentAccountToJdbcProcessStream = sys => - new PaymentAccountToJdbcProcessStream - with InMemoryJournalProvider - with InMemoryOffsetProvider - with PostgresProfile { - override implicit def system: ActorSystem[_] = sys - - override val forTests: Boolean = true - - override lazy val config: Config = PostgresPaymentTestKit.this.config - } + : ActorSystem[_] => Option[PaymentAccountToJdbcProcessStream] = sys => + Some( + new PaymentAccountToJdbcProcessStream + with InMemoryJournalProvider + with InMemoryOffsetProvider + with PostgresProfile { + override implicit def system: ActorSystem[_] = sys + override val forTests: Boolean = true + override lazy val config: Config = PostgresPaymentTestKit.this.config + } + ) override def transactionToJdbcProcessorStream : ActorSystem[_] => TransactionToJdbcProcessorStream = sys => @@ -58,9 +56,7 @@ trait PostgresPaymentTestKit extends JdbcPaymentTestKit with PostgresTestKit { with InMemoryOffsetProvider with PostgresProfile { override implicit def system: ActorSystem[_] = sys - override val forTests: Boolean = true - override lazy val config: Config = PostgresPaymentTestKit.this.config } } diff --git a/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentRouteTestKit.scala b/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentRouteTestKit.scala index f5fd9f8..c11d5dd 100644 --- a/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentRouteTestKit.scala +++ b/testkit/src/main/scala/app/softnetwork/payment/scalatest/StripePaymentRouteTestKit.scala @@ -170,7 +170,7 @@ trait StripePaymentRouteTestKit[SD <: SessionData with SessionDataDecorator[SD]] } } - private[this] def executeWebhook(payload: String): Unit = { + protected def executeWebhook(payload: String): Unit = { val stripeApi = StripeApi() stripeApi.secret match { case Some(secret) =>