Skip to content

Commit c181c70

Browse files
committed
STUD-450 | Added PayPal payments support
1 parent e940aca commit c181c70

24 files changed

Lines changed: 555 additions & 33 deletions

newm-server/src/main/kotlin/io/newm/server/Application.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import io.newm.server.features.earnings.createEarningsRoutes
2020
import io.newm.server.features.ethereum.createEthereumRoutes
2121
import io.newm.server.features.idenfy.createIdenfyRoutes
2222
import io.newm.server.features.marketplace.createMarketplaceRoutes
23+
import io.newm.server.features.paypal.createPayPalRoutes
2324
import io.newm.server.features.playlist.createPlaylistRoutes
2425
import io.newm.server.features.song.createSongRoutes
2526
import io.newm.server.features.user.createUserRoutes
@@ -90,6 +91,7 @@ fun Application.module() {
9091
createClientConfigRoutes()
9192
createOpenApiDocumentationRoutes()
9293
createEarningsRoutes()
94+
createPayPalRoutes()
9395
}
9496

9597
initializeDaemons()

newm-server/src/main/kotlin/io/newm/server/client/ClientKoinModule.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,22 @@ import io.ktor.client.plugins.auth.Auth
88
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
99
import io.ktor.serialization.kotlinx.json.json
1010
import io.newm.server.client.auth.appleMusicBearer
11+
import io.newm.server.client.auth.payPalBearer
1112
import io.newm.server.client.auth.soundCloudBearer
1213
import io.newm.server.client.auth.spotifyBearer
13-
import java.util.concurrent.TimeUnit
14-
import kotlin.time.Duration.Companion.minutes
15-
import kotlin.time.Duration.Companion.seconds
1614
import okhttp3.ConnectionPool
1715
import okhttp3.Protocol
1816
import org.koin.core.qualifier.named
1917
import org.koin.dsl.module
18+
import java.util.concurrent.TimeUnit
19+
import kotlin.time.Duration.Companion.minutes
20+
import kotlin.time.Duration.Companion.seconds
2021

2122
val QUALIFIER_SPOTIFY_HTTP_CLIENT = named("spotifyHttpClient")
2223
val QUALIFIER_APPLE_MUSIC_HTTP_CLIENT = named("appleMusicHttpClient")
2324
val QUALIFIER_SOUND_CLOUD_HTTP_CLIENT = named("soundCloudHttpClient")
2425
val QUALIFIER_UPLOAD_TRACK_HTTP_CLIENT = named("uploadTrackHttpClient")
26+
val QUALIFIER_PAYPAL_HTTP_CLIENT = named("payPalHttpClient")
2527

2628
val clientKoinModule =
2729
module {
@@ -91,4 +93,11 @@ val clientKoinModule =
9193
}
9294
}
9395
}
96+
single(QUALIFIER_PAYPAL_HTTP_CLIENT) {
97+
get<HttpClient>().config {
98+
install(Auth) {
99+
payPalBearer()
100+
}
101+
}
102+
}
94103
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.newm.server.client.auth
2+
3+
import io.ktor.client.HttpClient
4+
import io.ktor.client.plugins.auth.AuthConfig
5+
import io.ktor.client.plugins.auth.providers.BearerTokens
6+
import io.ktor.client.plugins.auth.providers.bearer
7+
import io.ktor.client.request.accept
8+
import io.ktor.client.request.basicAuth
9+
import io.ktor.client.request.forms.submitForm
10+
import io.ktor.http.ContentType
11+
import io.ktor.http.parameters
12+
import io.ktor.server.application.ApplicationEnvironment
13+
import io.newm.server.client.auth.model.TokenInfo
14+
import io.newm.server.ktx.checkedBody
15+
import io.newm.server.ktx.getSecureConfigString
16+
import io.newm.shared.koin.inject
17+
import io.newm.shared.ktx.getConfigString
18+
19+
fun AuthConfig.payPalBearer() =
20+
bearer {
21+
val loader = PayPalTokenLoader()
22+
23+
// Load and refresh tokens without waiting for a 401 first if the host matches
24+
sendWithoutRequest { request ->
25+
request.url.host.endsWith(".paypal.com")
26+
}
27+
loadTokens {
28+
loader.tokens ?: loader.load()
29+
}
30+
refreshTokens {
31+
loader.load()
32+
}
33+
}
34+
35+
private class PayPalTokenLoader {
36+
private val environment: ApplicationEnvironment by inject()
37+
private val httpClient: HttpClient by inject()
38+
var tokens: BearerTokens? = null
39+
40+
suspend fun load(): BearerTokens {
41+
val clientId = environment.getSecureConfigString("oauth.payPal.clientId")
42+
val clientSecret = environment.getSecureConfigString("oauth.payPal.clientSecret")
43+
val accessTokenUrl = environment.getConfigString("oauth.payPal.accessTokenUrl")
44+
val tokenInfo: TokenInfo =
45+
httpClient
46+
.submitForm(
47+
url = accessTokenUrl,
48+
formParameters =
49+
parameters {
50+
append("grant_type", "client_credentials")
51+
}
52+
) {
53+
basicAuth(clientId, clientSecret)
54+
accept(ContentType.Application.Json)
55+
}.checkedBody()
56+
return BearerTokens(tokenInfo.accessToken, tokenInfo.accessToken).also { tokens = it }
57+
}
58+
}

newm-server/src/main/kotlin/io/newm/server/di/DependencyInjectionInstall.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import io.newm.server.features.idenfy.idenfyKoinModule
2222
import io.newm.server.features.marketplace.marketplaceKoinModule
2323
import io.newm.server.features.minting.mintingKoinModule
2424
import io.newm.server.features.nftcdn.nftCdnKoinModule
25+
import io.newm.server.features.paypal.payPalKoinModule
2526
import io.newm.server.features.playlist.playlistKoinModule
2627
import io.newm.server.features.referralhero.referralHeroKoinModule
2728
import io.newm.server.features.release.releaseKoinModule
@@ -72,6 +73,7 @@ fun Application.installDependencyInjection() {
7273
nftCdnKoinModule,
7374
dripDropzKoinModule,
7475
referralHeroKoinModule,
76+
payPalKoinModule,
7577
)
7678
}
7779
}

newm-server/src/main/kotlin/io/newm/server/features/minting/MintingMessageReceiver.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import io.newm.server.features.song.repo.SongRepository
2222
import io.newm.server.features.user.repo.UserRepository
2323
import io.newm.server.logging.captureToSentry
2424
import io.newm.shared.koin.inject
25-
import kotlin.time.Duration.Companion.minutes
2625
import kotlinx.coroutines.coroutineScope
2726
import kotlinx.coroutines.launch
2827
import kotlinx.serialization.json.Json
@@ -31,6 +30,7 @@ import org.quartz.JobKey
3130
import org.quartz.SimpleScheduleBuilder.simpleSchedule
3231
import org.quartz.TriggerBuilder.newTrigger
3332
import software.amazon.awssdk.services.sqs.model.Message
33+
import kotlin.time.Duration.Companion.minutes
3434

3535
class MintingMessageReceiver : SqsMessageReceiver {
3636
private val log = KotlinLogging.logger {}
@@ -133,6 +133,12 @@ class MintingMessageReceiver : SqsMessageReceiver {
133133
}
134134
)
135135
}
136+
137+
PaymentType.PAYPAL -> {
138+
// unexpected, we never set MintingPaymentSubmitted for PayPal
139+
// probably a bug, this is handled gracefully by the outer try-catch
140+
throw IllegalStateException("Unexpected PayPal payment type for song: ${song.id}.")
141+
}
136142
}
137143
}
138144

@@ -144,6 +150,8 @@ class MintingMessageReceiver : SqsMessageReceiver {
144150
PaymentType.NEWM -> {
145151
log.info { "Awaiting NEWM payment for song: ${song.id} on address: ${monitorRequest.address} for ${monitorRequest.lovelace} lovelace and $cost NEWM." }
146152
}
153+
154+
PaymentType.PAYPAL -> {}
147155
}
148156

149157
val response = cardanoRepository.awaitPayment(monitorRequest)

newm-server/src/main/kotlin/io/newm/server/features/minting/repo/MintingRepositoryImpl.kt

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,13 @@ import io.newm.shared.ktx.toHexString
5656
import io.newm.txbuilder.ktx.sortByHashAndIx
5757
import io.newm.txbuilder.ktx.toCborObject
5858
import io.newm.txbuilder.ktx.toPlutusData
59-
import java.math.BigDecimal
60-
import kotlin.time.Duration.Companion.milliseconds
61-
import kotlin.time.Duration.Companion.seconds
6259
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
6360
import org.jetbrains.exposed.sql.transactions.transaction
6461
import org.koin.core.parameter.parametersOf
6562
import org.slf4j.Logger
63+
import java.math.BigDecimal
64+
import kotlin.time.Duration.Companion.milliseconds
65+
import kotlin.time.Duration.Companion.seconds
6666

6767
class MintingRepositoryImpl(
6868
private val userRepository: UserRepository,
@@ -136,6 +136,8 @@ class MintingRepositoryImpl(
136136
) { "NEWM payment UTXO not found or invalid for songId: ${song.id}, looking for $mintCost $newmPolicyId.$newmTokenName" }
137137
}
138138

139+
PaymentType.PAYPAL -> null // we'll fund the minting with cash register funds
140+
139141
else -> {
140142
// Handles PaymentType.ADA and legacy cases (where release.mintPaymentType might be null)
141143

@@ -177,7 +179,7 @@ class MintingRepositoryImpl(
177179
topLovelaceUtxos + topNewmUtxos
178180
}
179181

180-
else -> {
182+
else -> { // ADA or PayPal
181183
cardanoRepository
182184
.queryLiveUtxos(cashRegisterKey.address)
183185
.filter { it.nativeAssetsCount == 0 }
@@ -227,10 +229,7 @@ class MintingRepositoryImpl(
227229
// sort utxos lexicographically smallest to largest to find the one we'll use as the reference utxo
228230
val refUtxo =
229231
(
230-
cashRegisterUtxos + listOf(paymentUtxo) + (moneyBoxAdaUtxos ?: emptyList()) + (
231-
moneyBoxNewmUtxos
232-
?: emptyList()
233-
)
232+
cashRegisterUtxos + listOfNotNull(paymentUtxo) + moneyBoxAdaUtxos.orEmpty() + moneyBoxNewmUtxos.orEmpty()
234233
).sortByHashAndIx()
235234
.first()
236235

@@ -337,7 +336,7 @@ class MintingRepositoryImpl(
337336

338337
@VisibleForTesting
339338
internal suspend fun buildMintingTransaction(
340-
paymentUtxo: Utxo,
339+
paymentUtxo: Utxo?,
341340
cashRegisterUtxos: List<Utxo>,
342341
changeAddress: String,
343342
moneyBoxUtxos: List<Utxo>?,
@@ -359,7 +358,7 @@ class MintingRepositoryImpl(
359358
signatures: List<Signature> = emptyList()
360359
) = cardanoRepository.buildTransaction {
361360
with(sourceUtxos) {
362-
add(paymentUtxo)
361+
paymentUtxo?.let { add(it) }
363362
moneyBoxUtxos?.let { addAll(it) }
364363
addAll(cashRegisterUtxos)
365364
}
@@ -412,7 +411,7 @@ class MintingRepositoryImpl(
412411
asset.name == newmTokenName
413412
}
414413
}
415-
val paymentNewmUtxos = listOf(paymentUtxo).filter {
414+
val paymentNewmUtxos = listOfNotNull(paymentUtxo).filter {
416415
it.nativeAssetsCount == 1 &&
417416
it.nativeAssetsList.any { asset ->
418417
asset.policy == newmPolicyId &&
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.newm.server.features.paypal
2+
3+
import io.newm.server.client.QUALIFIER_PAYPAL_HTTP_CLIENT
4+
import io.newm.server.features.paypal.repo.PayPalRepository
5+
import io.newm.server.features.paypal.repo.PayPalRepositoryImpl
6+
import org.koin.dsl.module
7+
8+
val payPalKoinModule =
9+
module {
10+
single<PayPalRepository> { PayPalRepositoryImpl(get(), get(QUALIFIER_PAYPAL_HTTP_CLIENT), get()) }
11+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.newm.server.features.paypal
2+
3+
import io.ktor.http.HttpStatusCode
4+
import io.ktor.server.auth.authenticate
5+
import io.ktor.server.request.receive
6+
import io.ktor.server.response.respond
7+
import io.ktor.server.routing.Routing
8+
import io.ktor.server.routing.route
9+
import io.newm.server.auth.jwt.AUTH_JWT
10+
import io.newm.server.features.paypal.repo.PayPalRepository
11+
import io.newm.server.ktx.myUserId
12+
import io.newm.shared.koin.inject
13+
import io.newm.shared.ktx.post
14+
15+
fun Routing.createPayPalRoutes() {
16+
val repository: PayPalRepository by inject()
17+
18+
authenticate(AUTH_JWT) {
19+
route("v1/paypal/minting-distribution/orders") {
20+
post {
21+
respond(HttpStatusCode.Created, repository.createMintingDistributionOrder(myUserId, receive()))
22+
}
23+
post("{orderId}/capture") {
24+
repository.captureMintingDistributionOrder(parameters["orderId"]!!)
25+
respond(HttpStatusCode.OK)
26+
}
27+
}
28+
}
29+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.newm.server.features.paypal.model
2+
3+
import io.newm.server.typealiases.SongId
4+
import kotlinx.serialization.Contextual
5+
import kotlinx.serialization.Serializable
6+
7+
@Serializable
8+
data class MintingDistributionOrderRequest(
9+
@Contextual
10+
val songId: SongId
11+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package io.newm.server.features.paypal.model
2+
3+
import kotlinx.serialization.Serializable
4+
5+
@Serializable
6+
data class MintingDistributionOrderResponse(
7+
val orderId: String,
8+
val payerActionUrl: String
9+
)

0 commit comments

Comments
 (0)