Skip to content

Commit

Permalink
Add MercadoPago
Browse files Browse the repository at this point in the history
  • Loading branch information
MrPowerGamerBR committed Apr 23, 2024
1 parent a453388 commit 6d7b6ac
Show file tree
Hide file tree
Showing 12 changed files with 259 additions and 26 deletions.
1 change: 1 addition & 0 deletions backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dependencies {
implementation("org.jsoup:jsoup:1.14.3")
implementation("com.stripe:stripe-java:20.3.0")
implementation("com.paypal.sdk:checkout-sdk:1.0.5")
implementation("com.mercadopago:sdk-java:2.1.22")
implementation("club.minnced:discord-webhooks:0.7.5")

implementation(libs.kotlin.logging)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package net.perfectdreams.perfectpayments.backend

import club.minnced.discord.webhook.WebhookClient
import com.github.benmanes.caffeine.cache.Caffeine
import com.mercadopago.MercadoPagoConfig
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import io.ktor.client.*
Expand All @@ -24,22 +25,14 @@ import kotlinx.coroutines.withContext
import mu.KotlinLogging
import net.perfectdreams.perfectpayments.backend.config.AppConfig
import net.perfectdreams.perfectpayments.backend.config.FocusNFeConfig
import net.perfectdreams.perfectpayments.backend.processors.creators.PagSeguroPaymentCreator
import net.perfectdreams.perfectpayments.backend.processors.creators.PayPalPaymentCreator
import net.perfectdreams.perfectpayments.backend.processors.creators.PicPayPaymentCreator
import net.perfectdreams.perfectpayments.backend.processors.creators.SandboxPaymentCreator
import net.perfectdreams.perfectpayments.backend.processors.creators.StripePaymentCreator
import net.perfectdreams.perfectpayments.backend.processors.creators.*
import net.perfectdreams.perfectpayments.backend.routes.CancelledRoute
import net.perfectdreams.perfectpayments.backend.routes.HomeRoute
import net.perfectdreams.perfectpayments.backend.routes.MissingPartialPaymentRoute
import net.perfectdreams.perfectpayments.backend.routes.SuccessRoute
import net.perfectdreams.perfectpayments.backend.routes.api.v1.GetAvailableGatewaysRoute
import net.perfectdreams.perfectpayments.backend.routes.api.v1.GetStringsRoute
import net.perfectdreams.perfectpayments.backend.routes.api.v1.callbacks.PostFocusNFeCallbackRoute
import net.perfectdreams.perfectpayments.backend.routes.api.v1.callbacks.PostPagSeguroCallbackRoute
import net.perfectdreams.perfectpayments.backend.routes.api.v1.callbacks.PostPayPalCallbackRoute
import net.perfectdreams.perfectpayments.backend.routes.api.v1.callbacks.PostPicPayCallbackRoute
import net.perfectdreams.perfectpayments.backend.routes.api.v1.callbacks.PostStripeCallbackRoute
import net.perfectdreams.perfectpayments.backend.routes.api.v1.callbacks.*
import net.perfectdreams.perfectpayments.backend.routes.api.v1.payments.GetPartialPaymentInfoRoute
import net.perfectdreams.perfectpayments.backend.routes.api.v1.payments.GetReissueNotaFiscalForPaymentRoute
import net.perfectdreams.perfectpayments.backend.routes.api.v1.payments.GetRenotifyPaymentRoute
Expand Down Expand Up @@ -103,7 +96,8 @@ class PerfectPayments(
PaymentGateway.PAGSEGURO to PagSeguroPaymentCreator(this),
PaymentGateway.STRIPE to StripePaymentCreator(this),
PaymentGateway.PAYPAL to PayPalPaymentCreator(this),
PaymentGateway.SANDBOX to SandboxPaymentCreator(this)
PaymentGateway.SANDBOX to SandboxPaymentCreator(this),
PaymentGateway.MERCADOPAGO to MercadoPagoPaymentCreator(this)
)

val focusNFe = focusNFeConfig?.let {
Expand Down Expand Up @@ -151,6 +145,10 @@ class PerfectPayments(
it.add(PostPayPalCallbackRoute(this))
}

if (config.gateways.contains(PaymentGateway.MERCADOPAGO)) {
it.add(PostMercadoPagoCallbackRoute(this))
}

/* if (config.gateways.contains(PaymentGateway.SANDBOX))
it.add(PostCheckoutSandboxRoute(this)) */
}
Expand Down Expand Up @@ -203,6 +201,10 @@ class PerfectPayments(
scheduleCoroutineAtFixedRate(UpdatePagSeguroPaymentsTask::class.simpleName!!, tasksScope, 1.minutes, action = UpdatePagSeguroPaymentsTask(this))
}

if (config.gateways.contains(PaymentGateway.MERCADOPAGO)) {
MercadoPagoConfig.setAccessToken(gateway.mercadoPago.accessToken) // Nasty!!
}

val server = embeddedServer(Netty, host = config.website.host, port = config.website.port) {
install(CORS) {
anyHost()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.hocon.Hocon
import kotlinx.serialization.hocon.decodeFromConfig
import mu.KotlinLogging
import net.perfectdreams.perfectpayments.backend.config.AppConfig
import net.perfectdreams.perfectpayments.backend.config.FocusNFeConfig
import net.perfectdreams.perfectpayments.backend.config.PagSeguroConfig
import net.perfectdreams.perfectpayments.backend.config.PayPalConfig
import net.perfectdreams.perfectpayments.backend.config.PicPayConfig
import net.perfectdreams.perfectpayments.backend.config.StripeConfig
import net.perfectdreams.perfectpayments.backend.config.*
import net.perfectdreams.perfectpayments.common.payments.PaymentGateway
import java.io.File

Expand Down Expand Up @@ -41,6 +36,9 @@ object PerfectPaymentsLauncher {
if (gateway == PaymentGateway.PAYPAL) {
configs[gateway] = loadConfig<PayPalConfig>("./paypal.conf")
}
if (gateway == PaymentGateway.MERCADOPAGO) {
configs[gateway] = loadConfig<MercadoPagoConfig>("./mercadopago.conf")
}
}

val focusNFeConfig = loadConfig<FocusNFeConfig>("./focusnfe.conf")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.perfectdreams.perfectpayments.backend.config

import kotlinx.serialization.Serializable

@Serializable
class MercadoPagoConfig(
val accessToken: String,
val webhookSecretSignature: String,
val callbackUrl: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,9 @@ class CreatedPicPayPaymentInfo(

class CreatedSandboxPaymentInfo(
id: String
) : CreatedPaymentInfo(id)
) : CreatedPaymentInfo(id)

class CreatedMercadoPagoPaymentInfo(
id: String,
url: String
) : CreatedPaymentInfoWithUrl(id, url)
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package net.perfectdreams.perfectpayments.backend.processors.creators

import com.mercadopago.client.preference.PreferenceClient
import com.mercadopago.client.preference.PreferenceItemRequest
import com.mercadopago.client.preference.PreferenceRequest
import kotlinx.serialization.json.JsonObject
import net.perfectdreams.perfectpayments.backend.PerfectPayments
import net.perfectdreams.perfectpayments.backend.utils.PartialPayment
import net.perfectdreams.perfectpayments.backend.utils.TextUtils
import java.math.BigDecimal

class MercadoPagoPaymentCreator(val m: PerfectPayments) : PaymentCreator {
val client = PreferenceClient()

override suspend fun createPayment(paymentId: Long, partialPayment: PartialPayment, data: JsonObject): CreatedMercadoPagoPaymentInfo {
val itemRequest =
PreferenceItemRequest.builder()
.title(TextUtils.cleanTitle(partialPayment.title))
.quantity(1)
.currencyId("BRL")
.unitPrice(BigDecimal(partialPayment.amount / 100.0))
.build()
val items: MutableList<PreferenceItemRequest> = ArrayList()
items.add(itemRequest)
val preferenceRequest = PreferenceRequest.builder()
.externalReference(partialPayment.externalReference.format(paymentId))
.notificationUrl(m.gateway.mercadoPago.callbackUrl)
.items(items)
.build()
val preference = client.create(preferenceRequest)

return CreatedMercadoPagoPaymentInfo(
preference.id,
preference.initPoint
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package net.perfectdreams.perfectpayments.backend.routes.api.v1.callbacks

import com.mercadopago.client.payment.PaymentClient
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import mu.KotlinLogging
import net.perfectdreams.perfectpayments.backend.PerfectPayments
import net.perfectdreams.perfectpayments.backend.dao.Payment
import net.perfectdreams.perfectpayments.backend.payments.PaymentStatus
import net.perfectdreams.perfectpayments.backend.utils.PaymentUtils
import net.perfectdreams.perfectpayments.backend.utils.extensions.respondEmptyJson
import net.perfectdreams.sequins.ktor.BaseRoute
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec


class PostMercadoPagoCallbackRoute(val m: PerfectPayments) : BaseRoute("/api/v1/callbacks/mercadopago") {
companion object {
private val logger = KotlinLogging.logger {}
}

private val paymentClient = PaymentClient()

override suspend fun onRequest(call: ApplicationCall) {
logger.info { "Received MercadoPago Webhook Request" }

val parameters = call.request.queryParameters
val body = call.receiveText()
val type = parameters["type"]

logger.info { "MercadoPago type: $type, params: ${parameters.entries()}; body: $body" }

val xSignature = call.request.header("x-signature")
val xRequestId = call.request.header("x-request-id")

if (xSignature == null) {
logger.warn { "MercadoPago request is missing the x-signature header!" }
call.respondEmptyJson(HttpStatusCode.Forbidden)
return
}

if (xRequestId == null) {
logger.warn { "MercadoPago request is missing the x-request-id header!" }
call.respondEmptyJson(HttpStatusCode.Forbidden)
return
}

if (!validate(parameters["data.id"], xSignature, xRequestId)) {
logger.warn { "MercadoPago request didn't match our signature!" }
call.respondEmptyJson(HttpStatusCode.Forbidden)
return
}

when (type) {
"payment" -> {
// Get payment info
val dataId = parameters["data.id"]?.toLongOrNull() ?: error("Missing data.id!")
val payment = paymentClient.get(dataId)

val reference = payment.externalReference
val internalTransactionId = reference.split("-").last()

val internalPayment = m.newSuspendedTransaction {
Payment.findById(internalTransactionId.toLong())
}

if (internalPayment == null) {
logger.warn { "MercadoPago Payment with Reference ID: $reference ($internalTransactionId) doesn't have a matching internal ID! Bug?" }
call.respondEmptyJson()
return
}

when (payment.status) {
"approved" -> {
PaymentUtils.updatePaymentStatus(
m,
internalPayment,
PaymentStatus.APPROVED
)
}
"in_mediation" -> {
PaymentUtils.updatePaymentStatus(
m,
internalPayment,
PaymentStatus.CHARGED_BACK
)
}
"charged_back" -> {
PaymentUtils.updatePaymentStatus(
m,
internalPayment,
PaymentStatus.CHARGED_BACK
)
}
}
}
}

call.respondEmptyJson()
}

fun validate(dataID: String?, xSignature: String, xRequestId: String): Boolean {
// Separating the x-signature into parts
val parts = xSignature.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()

// Initializing variables to store ts and hash
var ts: String? = null
var hash: String? = null

// Iterate over the values to obtain ts and v1
for (part in parts) {
val keyValue = part.trim { it <= ' ' }.split("=".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
if (keyValue.size == 2) {
val key = keyValue[0].trim { it <= ' ' }
val value = keyValue[1].trim { it <= ' ' }
if ("ts" == key) {
ts = value
} else if ("v1" == key) {
hash = value
}
}
}

// Generate the manifest string
val manifest = String.format("id:%s;request-id:%s;ts:%s;", dataID, xRequestId, ts)

val mac = Mac.getInstance("HmacSHA256")

val signingKey = SecretKeySpec(m.gateway.mercadoPago.webhookSecretSignature.toByteArray(Charsets.UTF_8), "HmacSHA256")
mac.init(signingKey)
val doneFinal = mac.doFinal(manifest.toByteArray(Charsets.UTF_8))

return hash == doneFinal.bytesToHex()
}

/**
* Converts a ByteArray to a hexadecimal string
*
* @return the byte array in hexadecimal format
*/
private fun ByteArray.bytesToHex(): String {
val hexString = StringBuffer()
for (i in this.indices) {
val hex = Integer.toHexString(0xff and this[i].toInt())
if (hex.length == 1) {
hexString.append('0')
}
hexString.append(hex)
}
return hexString.toString()
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package net.perfectdreams.perfectpayments.backend.utils

import net.perfectdreams.perfectpayments.backend.config.PagSeguroConfig
import net.perfectdreams.perfectpayments.backend.config.PayPalConfig
import net.perfectdreams.perfectpayments.backend.config.PicPayConfig
import net.perfectdreams.perfectpayments.backend.config.StripeConfig
import net.perfectdreams.perfectpayments.backend.config.*
import net.perfectdreams.perfectpayments.common.payments.PaymentGateway

class GatewayConfigs(val map: Map<PaymentGateway, Any>) {
Expand All @@ -15,4 +12,6 @@ class GatewayConfigs(val map: Map<PaymentGateway, Any>) {
get() = map[PaymentGateway.STRIPE] as StripeConfig
val payPal: PayPalConfig
get() = map[PaymentGateway.PAYPAL] as PayPalConfig
val mercadoPago: MercadoPagoConfig
get() = map[PaymentGateway.MERCADOPAGO] as MercadoPagoConfig
}
Loading

0 comments on commit 6d7b6ac

Please sign in to comment.