Skip to content
This repository has been archived by the owner on Feb 19, 2025. It is now read-only.

Commit

Permalink
Merge pull request #59 from nathanfallet/feature/stripe-webhook
Browse files Browse the repository at this point in the history
Feature/stripe webhook
  • Loading branch information
nathanfallet authored Feb 25, 2024
2 parents 1eb0a47 + 21a2a06 commit a29cf48
Show file tree
Hide file tree
Showing 44 changed files with 619 additions and 194 deletions.
9 changes: 9 additions & 0 deletions helm/suitebde/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,15 @@ spec:
name: {{ .Values.existingSecret }}
{{- end }}
key: stripe-key
- name: STRIPE_SECRET
valueFrom:
secretKeyRef:
{{ if not .Values.existingSecret -}}
name: {{ include "suitebde.fullname" . }}
{{- else }}
name: {{ .Values.existingSecret }}
{{- end }}
key: stripe-secret
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
Expand Down
1 change: 1 addition & 0 deletions helm/suitebde/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ data:
cloudflare-account: {{ .Values.cloudflare.account | b64enc | quote }}
cloudflare-token: {{ .Values.cloudflare.token | b64enc | quote }}
stripe-key: {{ .Values.stripe.key | b64enc | quote }}
stripe-secret: {{ .Values.stripe.secret | b64enc | quote }}
{{ end }}
1 change: 1 addition & 0 deletions helm/suitebde/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ cloudflare:

stripe:
key: ''
secret: ''

# MySQL configuration

Expand Down
1 change: 1 addition & 0 deletions suitebde-backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ kotlin {
implementation("org.apache.commons:commons-email:1.5")
implementation("io.sentry:sentry:6.34.0")
implementation("com.stripe:stripe-java:24.17.0")
implementation("com.google.code.gson:gson:2.10.1")

api(project(":suitebde-commons"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package me.nathanfallet.suitebde.controllers.dashboard
import io.ktor.server.application.*
import io.ktor.server.request.*
import me.nathanfallet.ktorx.models.responses.RedirectResponse
import me.nathanfallet.suitebde.usecases.associations.ICreateStripeAccountLinkUseCase
import me.nathanfallet.suitebde.usecases.associations.IRefreshStripeAccountUseCase
import me.nathanfallet.suitebde.usecases.associations.IRequireAssociationForCallUseCase
import me.nathanfallet.suitebde.usecases.stripe.ICreateStripeAccountLinkUseCase
import me.nathanfallet.suitebde.usecases.stripe.IRefreshStripeAccountUseCase

class DashboardController(
private val requireAssociationForCallUseCase: IRequireAssociationForCallUseCase,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package me.nathanfallet.suitebde.controllers.webhooks

import io.ktor.server.application.*
import me.nathanfallet.ktorx.controllers.IUnitController
import me.nathanfallet.ktorx.models.annotations.APIMapping
import me.nathanfallet.ktorx.models.annotations.Path

interface IWebhooksController : IUnitController {

@APIMapping
@Path("POST", "/stripe")
suspend fun stripe(call: ApplicationCall)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package me.nathanfallet.suitebde.controllers.webhooks

import com.stripe.model.checkout.Session
import com.stripe.net.Webhook
import io.ktor.server.application.*
import io.ktor.server.request.*
import kotlinx.datetime.Clock
import me.nathanfallet.suitebde.models.stripe.StripeOrder
import me.nathanfallet.suitebde.models.stripe.UpdateStripeOrderPayload
import me.nathanfallet.suitebde.usecases.stripe.ICreateStripeOrderForSessionUseCase
import me.nathanfallet.usecases.models.update.IUpdateChildModelSuspendUseCase

class WebhooksController(
private val stripeSecret: String,
private val createStripeOrderForSessionUseCase: ICreateStripeOrderForSessionUseCase,
private val updateStripeOrderUseCase: IUpdateChildModelSuspendUseCase<StripeOrder, String, UpdateStripeOrderPayload, String>,
) : IWebhooksController {

override suspend fun stripe(call: ApplicationCall) {
// Decode event (payload)
val payload = call.receiveText()
val signature = call.request.headers["Stripe-Signature"]
val event = Webhook.constructEvent(payload, signature, stripeSecret)

// Decode event data
val stripeObject = event.dataObjectDeserializer.`object`.get()
when (event.type) {
"checkout.session.completed" -> {
val session = stripeObject as Session
createStripeOrderForSessionUseCase(session) ?: return
if (session.paymentStatus == "paid") updateStripeOrderUseCase(
session.id,
UpdateStripeOrderPayload(Clock.System.now()),
session.metadata["associationId"]!!
)
}

"checkout.session.async_payment_succeeded" -> {
val session = stripeObject as Session
updateStripeOrderUseCase(
session.id,
UpdateStripeOrderPayload(Clock.System.now()),
session.metadata["associationId"]!!
)
}

"checkout.session.async_payment_failed" -> {
val session = stripeObject as Session
// Payment failed
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package me.nathanfallet.suitebde.controllers.webhooks

import me.nathanfallet.ktorx.routers.api.APIUnitRouter

class WebhooksRouter(
controller: IWebhooksController,
) : APIUnitRouter(
controller,
IWebhooksController::class,
route = "webhooks",
prefix = "/api/v1"
)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
package me.nathanfallet.suitebde.database.associations
package me.nathanfallet.suitebde.database.stripe

import me.nathanfallet.suitebde.models.associations.StripeAccountInAssociation
import me.nathanfallet.suitebde.models.stripe.StripeAccount
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.Table

object StripeAccountsInAssociations : Table() {
object StripeAccounts : Table() {

val associationId = varchar("association_id", 32).index()
val accountId = varchar("account_id", 255).index()
val chargesEnabled = bool("charges_enabled")

override val primaryKey = PrimaryKey(associationId, accountId)

fun toStripeAccountInAssociation(
fun toStripeAccount(
row: ResultRow,
) = StripeAccountInAssociation(
) = StripeAccount(
row[associationId],
row[accountId],
row[chargesEnabled]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package me.nathanfallet.suitebde.database.stripe

import me.nathanfallet.suitebde.models.stripe.CreateStripeAccountPayload
import me.nathanfallet.suitebde.models.stripe.StripeAccount
import me.nathanfallet.suitebde.models.stripe.UpdateStripeAccountPayload
import me.nathanfallet.suitebde.repositories.stripe.IStripeAccountsRepository
import me.nathanfallet.surexposed.database.IDatabase
import me.nathanfallet.usecases.context.IContext
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq

class StripeAccountsDatabaseRepository(
private val database: IDatabase,
) : IStripeAccountsRepository {

init {
database.transaction {
SchemaUtils.create(StripeAccounts)
}
}

override suspend fun list(parentId: String, context: IContext?): List<StripeAccount> =
database.suspendedTransaction {
StripeAccounts
.selectAll()
.where { StripeAccounts.associationId eq parentId }
.map(StripeAccounts::toStripeAccount)
}

override suspend fun create(
payload: CreateStripeAccountPayload,
parentId: String,
context: IContext?,
): StripeAccount? =
database.suspendedTransaction {
StripeAccounts.insert {
it[associationId] = parentId
it[accountId] = payload.accountId
it[chargesEnabled] = payload.chargesEnabled
}.resultedValues?.map(StripeAccounts::toStripeAccount)?.singleOrNull()
}

override suspend fun get(id: String, parentId: String, context: IContext?): StripeAccount? =
database.suspendedTransaction {
StripeAccounts
.selectAll()
.where {
StripeAccounts.accountId eq id and (StripeAccounts.associationId eq parentId)
}
.map(StripeAccounts::toStripeAccount)
.singleOrNull()
}

override suspend fun update(
id: String,
payload: UpdateStripeAccountPayload,
parentId: String,
context: IContext?,
): Boolean =
database.suspendedTransaction {
StripeAccounts.update({ StripeAccounts.accountId eq id and (StripeAccounts.associationId eq parentId) }) {
it[chargesEnabled] = payload.chargesEnabled
} == 1
}

override suspend fun delete(id: String, parentId: String, context: IContext?): Boolean =
database.suspendedTransaction {
StripeAccounts.deleteWhere {
accountId eq id and (associationId eq parentId)
} == 1
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package me.nathanfallet.suitebde.database.stripe

import kotlinx.datetime.toInstant
import me.nathanfallet.suitebde.models.application.SuiteBDEJson
import me.nathanfallet.suitebde.models.stripe.StripeOrder
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.Table

object StripeOrders : Table() {

val sessionId = varchar("session_id", 255)
val associationId = varchar("association_id", 32).index()
val email = varchar("email", 255)
val items = text("items")
val paidAt = varchar("paid_at", 255).nullable()

override val primaryKey = PrimaryKey(sessionId)

fun toStripeOrder(
row: ResultRow,
) = StripeOrder(
row[sessionId],
row[associationId],
row[email],
SuiteBDEJson.json.decodeFromString(row[items]),
row[paidAt]?.toInstant()
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package me.nathanfallet.suitebde.database.stripe

import kotlinx.serialization.encodeToString
import me.nathanfallet.suitebde.models.application.SuiteBDEJson
import me.nathanfallet.suitebde.models.stripe.CreateStripeOrderPayload
import me.nathanfallet.suitebde.models.stripe.StripeOrder
import me.nathanfallet.suitebde.models.stripe.UpdateStripeOrderPayload
import me.nathanfallet.suitebde.repositories.stripe.IStripeOrdersRepository
import me.nathanfallet.surexposed.database.IDatabase
import me.nathanfallet.usecases.context.IContext
import org.jetbrains.exposed.sql.*

class StripeOrdersDatabaseRepository(
private val database: IDatabase,
) : IStripeOrdersRepository {

init {
database.transaction {
SchemaUtils.create(StripeOrders)
}
}

override suspend fun get(id: String, parentId: String, context: IContext?): StripeOrder? =
database.suspendedTransaction {
StripeOrders
.selectAll()
.where {
StripeOrders.sessionId eq id and (StripeOrders.associationId eq parentId)
}
.map(StripeOrders::toStripeOrder)
.singleOrNull()
}

override suspend fun create(payload: CreateStripeOrderPayload, parentId: String, context: IContext?): StripeOrder? =
database.suspendedTransaction {
StripeOrders.insert {
it[sessionId] = payload.sessionId
it[associationId] = parentId
it[email] = payload.email
it[items] = SuiteBDEJson.json.encodeToString(payload.items)
}.resultedValues?.map(StripeOrders::toStripeOrder)?.singleOrNull()
}

override suspend fun update(
id: String,
payload: UpdateStripeOrderPayload,
parentId: String,
context: IContext?,
): Boolean =
database.suspendedTransaction {
StripeOrders.update({ StripeOrders.sessionId eq id and (StripeOrders.associationId eq parentId) }) {
it[paidAt] = payload.paidAt?.toString()
} == 1
}

}
Loading

0 comments on commit a29cf48

Please sign in to comment.