Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ ThisBuild / organization := "app.softnetwork"

name := "payment"

ThisBuild / version := "0.9.4.1"
ThisBuild / version := "0.9.5"

ThisBuild / scalaVersion := scala212

Expand Down
2 changes: 2 additions & 0 deletions common/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ payment{
declaration-route = "declaration"
kyc-route = "kyc"

billing-portal-route = "billing-portal"

disable-bank-account-deletion = false

akka-node-role = payment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ object Payment {
accountRoute: String,
declarationRoute: String,
kycRoute: String,
billingPortalRoute: String,
disableBankAccountDeletion: Boolean,
externalToPaymentAccountTag: String,
akkaNodeRole: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,26 @@ object PaymentMessages {
lazy val key: String = account
}

case class BillingPortalRequest(returnUrl: String)

/** Create a billing portal session for the authenticated user.
*
* @param account
* - payment account reference (externalUuid)
* @param returnUrl
* - the URL to redirect to after the user leaves the portal
* @param clientId
* - optional client id
*/
case class CreateBillingPortalSession(
account: String,
returnUrl: String,
clientId: Option[String] = None
) extends PaymentCommandWithKey
with PaymentAccountCommand {
lazy val key: String = account
}

/** @param transactionId
* - transaction id
*/
Expand Down Expand Up @@ -1115,6 +1135,8 @@ object PaymentMessages {

case class BalanceLoaded(balance: Int) extends PaymentResult

case class BillingPortalSessionCreated(url: String) extends PaymentResult

class PaymentError(override val message: String) extends ErrorMessage(message) with PaymentResult

case object PaymentMethodNotPreRegistered extends PaymentError("PaymentMethodNotPreRegistered")
Expand Down Expand Up @@ -1155,6 +1177,8 @@ object PaymentMessages {

case object PaymentAccountNotFound extends PaymentError("PaymentAccountNotFound")

case object BillingPortalSessionNotCreated extends PaymentError("BillingPortalSessionNotCreated")

case object MandateAlreadyExists extends PaymentError("MandateAlreadyExists")

case class MandateCreationFailed(errorCode: String, errorMessage: String)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ trait BasicPaymentService extends Service[PaymentCommand, PaymentResult] {
ApiErrors.InternalServerError(PaymentMethodNotPreRegistered)
case r: PreAuthorizationFailed => ApiErrors.InternalServerError(r)
case r: PayInFailed => ApiErrors.InternalServerError(r)
case r: PaymentError => ApiErrors.BadRequest(r.message)
case _ => ApiErrors.BadRequest("Unknown")
case BillingPortalSessionNotCreated =>
ApiErrors.InternalServerError(BillingPortalSessionNotCreated)
case r: PaymentError => ApiErrors.BadRequest(r.message)
case _ => ApiErrors.BadRequest("Unknown")
}

protected[payment] def extractBrowserInfo(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package app.softnetwork.payment.spi

trait BillingPortalApi { _: PaymentContext =>

/** Create a billing portal session for the given user.
*
* @param userId
* \- the provider user ID
* @param returnUrl
* \- the URL to redirect to after the user leaves the portal
* @return
* the billing portal session URL, or None on failure
*/
def createBillingPortalSession(userId: String, returnUrl: String): Option[String]
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ private[payment] trait PaymentProvider
with TransferApi
with RefundApi
with RecurringPaymentApi
with BalanceApi {
with BalanceApi
with BillingPortalApi {

protected lazy val mlog: Logger = Logger(LoggerFactory.getLogger(getClass.getName))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,29 @@ trait PaymentBehavior
case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo)
}

case cmd: CreateBillingPortalSession =>
state match {
case Some(paymentAccount) =>
val userId = paymentAccount.getNaturalUser.userId
.orElse(paymentAccount.getLegalUser.legalRepresentative.userId)
userId match {
case Some(uid) =>
val clientId = paymentAccount.clientId.orElse(
internalClientId
)
val paymentProvider = loadPaymentProvider(clientId)
paymentProvider.createBillingPortalSession(uid, cmd.returnUrl) match {
case Some(url) =>
Effect.none.thenRun(_ => BillingPortalSessionCreated(url) ~> replyTo)
case None =>
Effect.none.thenRun(_ => BillingPortalSessionNotCreated ~> replyTo)
}
case None =>
Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo)
}
case _ => Effect.none.thenRun(_ => PaymentAccountNotFound ~> replyTo)
}

case cmd: LoadTransaction =>
import cmd._
state match {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package app.softnetwork.payment.service

import app.softnetwork.payment.config.PaymentSettings
import app.softnetwork.payment.handlers.PaymentHandler
import app.softnetwork.payment.message.PaymentMessages._
import app.softnetwork.session.model.{SessionData, SessionDataDecorator}
import sttp.capabilities
import sttp.capabilities.akka.AkkaStreams
import sttp.model.StatusCode
import sttp.tapir.json.json4s.jsonBody
import sttp.tapir.server.ServerEndpoint

import scala.concurrent.Future

trait BillingPortalEndpoints[SD <: SessionData with SessionDataDecorator[SD]] {
_: RootPaymentEndpoints[SD] with PaymentHandler =>

import app.softnetwork.serialization._

val createBillingPortalSession: ServerEndpoint[Any with AkkaStreams, Future] =
requiredSessionEndpoint.post
.in(PaymentSettings.PaymentConfig.billingPortalRoute)
.in(jsonBody[BillingPortalRequest])
.out(
statusCode(StatusCode.Ok)
.and(jsonBody[BillingPortalSessionCreated])
)
.serverLogic { case (client, session) =>
req => {
run(
CreateBillingPortalSession(
externalUuidWithProfile(session),
req.returnUrl,
clientId = client.map(_.clientId).orElse(session.clientId)
)
).map {
case r: BillingPortalSessionCreated => Right(r)
case other => Left(error(other))
}
}
}
.description("Create a billing portal session for the authenticated user")

val billingPortalEndpoints
: List[ServerEndpoint[AkkaStreams with capabilities.WebSockets, Future]] =
List(createBillingPortalSession)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ trait PaymentServiceEndpoints[SD <: SessionData with SessionDataDecorator[SD]]
with KycDocumentEndpoints[SD]
with UboDeclarationEndpoints[SD]
with RecurringPaymentEndpoints[SD]
with MandateEndpoints[SD] {
with MandateEndpoints[SD]
with BillingPortalEndpoints[SD] {
_: PaymentHandler with SessionMaterials[SD] =>

/** should be implemented by each payment provider
Expand All @@ -43,6 +44,7 @@ trait PaymentServiceEndpoints[SD <: SessionData with SessionDataDecorator[SD]]
uboDeclarationEndpoints ++
mandateEndpoints ++
recurringPaymentEndpoints ++
billingPortalEndpoints ++
hooks

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package app.softnetwork.payment.service

import app.softnetwork.account.config.AccountSettings
import app.softnetwork.account.message.{LoadProfile, ProfileLoaded}
import app.softnetwork.account.model.{DefaultProfileView, ProfileType}
import app.softnetwork.account.service.BasicAccountServiceEndpoints
import app.softnetwork.api.server.ApiErrors
import app.softnetwork.payment.handlers.SoftPayAccountTypeKey
import app.softnetwork.payment.serialization.paymentFormats
import app.softnetwork.session.config.Settings
Expand All @@ -10,7 +14,9 @@ import com.softwaremill.session.SessionConfig
import org.json4s.Formats
import sttp.capabilities
import sttp.capabilities.akka.AkkaStreams
import sttp.tapir.json.json4s.jsonBody
import sttp.tapir.server.ServerEndpoint
import sttp.tapir.{query, Schema}

import scala.concurrent.Future

Expand All @@ -22,9 +28,33 @@ trait SoftPayAccountServiceEndpoints[SD <: SessionData with SessionDataDecorator

override implicit lazy val formats: Formats = paymentFormats

import app.softnetwork.serialization.serialization

implicit lazy val profileTypeSchema: Schema[ProfileType] = Schema.derived
implicit lazy val defaultProfileViewSchema: Schema[DefaultProfileView] = Schema.derived

lazy val loadProfile: ServerEndpoint[Any with AkkaStreams, Future] =
ApiErrors
.withApiErrorVariants(
antiCsrfWithRequiredSession(sc, gt, checkMode)
)
.in(AccountSettings.Path / "profile")
.in(query[Option[String]]("name"))
.get
.out(jsonBody[DefaultProfileView])
.serverLogic(session =>
name =>
run(session.id, LoadProfile(session.id, name)).map {
case r: ProfileLoaded =>
Right(r.profile.view.asInstanceOf[DefaultProfileView])
case other => Left(resultToApiError(other))
}
)

override lazy val endpoints
: List[ServerEndpoint[AkkaStreams with capabilities.WebSockets, Future]] =
List(
loadProfile,
signUp,
login,
signIn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2699,6 +2699,20 @@ trait MangoPayProvider extends PaymentProvider {
None
}
}

/** Create a billing portal session for the given user.
*
* @param userId
* \- the provider user ID
* @param returnUrl
* \- the URL to redirect to after the user leaves the portal
* @return
* the billing portal session URL, or None on failure
*/
override def createBillingPortalSession(userId: String, returnUrl: String): Option[String] = {
mlog.warn("Billing portal is not supported by MangoPay provider")
None
}
}

class MangoPayProviderFactory extends PaymentProviderSpi {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package app.softnetwork.payment.spi

import app.softnetwork.payment.config.StripeApi

import scala.util.{Failure, Success, Try}

trait StripeBillingPortalApi extends BillingPortalApi { _: StripeContext =>

override def createBillingPortalSession(userId: String, returnUrl: String): Option[String] = {
Try {
val params = com.stripe.param.billingportal.SessionCreateParams
.builder()
.setCustomer(userId)
.setReturnUrl(returnUrl)
.build()
com.stripe.model.billingportal.Session.create(params, StripeApi().requestOptions())
} match {
case Success(session) => Some(session.getUrl)
case Failure(f) =>
mlog.error(s"Failed to create billing portal session for $userId: ${f.getMessage}", f)
None
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ trait StripeProvider
with StripeRecurringPaymentApi
with StripeRefundApi
with StripeTransferApi
with StripeBalanceApi {
with StripeBalanceApi
with StripeBillingPortalApi {

/** @return
* client
Expand Down
Loading