Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Mercadopago Integration #926

Merged
merged 13 commits into from
Jul 24, 2024
225 changes: 225 additions & 0 deletions app/Extensions/PaymentGateways/MercadoPago/MercadoPagoExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
<?php

namespace App\Extensions\PaymentGateways\MercadoPago;

use App\Classes\AbstractExtension;
use App\Enums\PaymentStatus;
use App\Events\PaymentEvent;
use App\Events\UserUpdateCreditsEvent;
use App\Models\Payment;
use App\Models\ShopProduct;
use App\Models\User;
use App\Traits\Coupon as CouponTrait;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
use App\Notifications\ConfirmPaymentNotification;

/**
* Summary of MercadoPagoExtension
*/
class MercadoPagoExtension extends AbstractExtension
{
use CouponTrait;

public static function getConfig(): array
{
return [
"name" => "MercadoPago",
"RoutesIgnoreCsrf" => [
"payment/MercadoPagoWebhook"
],
];
}

public static function getRedirectUrl(Payment $payment, ShopProduct $shopProduct, string $totalPriceString): string
{
$user = Auth::user();
$user = User::findOrFail($user->id);
$url = 'https://api.mercadopago.com/checkout/preferences';
$settings = new MercadoPagoSettings();
try {
$response = Http::withHeaders([
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $settings->access_token,
])->post($url, [
'back_urls' => [
'success' => route('payment.MercadoPagoChecker'),
'failure' => route('payment.Cancel'),
'pending' => route('payment.MercadoPagoChecker'),
],
'notification_url' => route('payment.MercadoPagoWebhook'),
'payer' => [
'email' => $user->email,
],
'items' => [
[
'title' => "Order #{$payment->id} - " . $shopProduct->name,
'quantity' => 1,
'unit_price' => $totalPriceString,
'currency_id' => $shopProduct->currency_code,
],
],
'metadata' => [
'credit_amount' => $shopProduct->quantity,
'user_id' => $user->id,
'crtl_panel_payment_id' => $payment->id,
],
]);

if ($response->successful()) {
// preferenceID
$preferenceId = $response->json()['id'];

// Redirect link
return ("https://www.mercadopago.com/checkout/v1/redirect?preference-id=" . $preferenceId);
} else {
Log::error('MercadoPago Payment: ' . $response->body());
throw new Exception('Payment failed');
}
} catch (Exception $ex) {
Log::error('MercadoPago Payment: ' . $ex->getMessage());
throw new Exception('Payment failed');
}
}

static function Checker(Request $request): void
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might want to follow the naming scheme already used in other payment gateways.

{
// paymentID (not is preferenceID or paymentID for store)
$paymentId = $request->input('payment_id');

$MpPayment = self::MpPayment($paymentId, false);

switch ($MpPayment) {
case "paid":
Redirect::route('home')->with('success', 'Payment successful')->send();
break;
case "cancelled":
Redirect::route('home')->with('info', 'Your canceled the payment')->send();
break;
case "processing":
Redirect::route('home')->with('info', 'Your payment is being processed')->send();
break;
default:
Redirect::route('home')->with('error', 'Your payment is unknown')->send();
break;
}
}

static function Webhook(Request $request): JsonResponse
{
$topic = $request->input('topic');
$msg = 'unset';
$status = 400;
if ($topic === 'merchant_order') {
$msg = 'ignored';
$status = 200;
} else if ($topic === 'payment') {
$msg = 'ignored';
$status = 200;
} else {
try {
$notificationId = $request->input('data.id') ?? $request->input('id') ?? $request->input('payment_id') ?? 'unknown';
if ($notificationId == 'unknown') {
$msg = 'unknown payment.';
$status = 400;
} else if ($notificationId == '123456') {
$msg = 'MercadoPago api test';
$status = 200;
} else {
$MpPayment = self::MpPayment($notificationId, true);
switch ($MpPayment) {
case "paid":
$msg = $MpPayment;
$status = 200;
break;

case "cancelled":
$msg = $MpPayment;
$status = 200;
break;

case "processing":
$msg = $MpPayment;
$status = 200;
break;
default:
$msg = 'unknown';
$status = 400;
break;
}
}
} catch (Exception $ex) {
Log::error('MercadoPago Webhook(IPN) Payment: ' . $ex->getMessage());
$msg = 'error';
$status = 500;
}
}
$response = new JsonResponse($msg, $status);
return $response;
}
/**
* Mercado Pago Payment checker
*/
private function MpPayment(string $paymentID, bool $notification): string
{
$MpResponse = "unknown";
$payment = "unknown";
$url = "https://api.mercadopago.com/v1/payments/" . $paymentID;
$settings = new MercadoPagoSettings();
$response = Http::withHeaders([
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $settings->access_token,
])->get($url);

if ($response->successful()) {
$mercado = $response->json();
$status = $mercado->status;
$payment = Payment::findOrFail($mercado->metadata->crtl_panel_payment_id);
$shopProduct = ShopProduct::findOrFail($payment->shop_item_product_id);

if ($status == "approved") {
// avoids double additions, if the user enters after the webhook has already added the credits
if ($payment->status !== PaymentStatus::PAID) {
$user = User::findOrFail($payment->user_id);
$payment->update([
'status' => PaymentStatus::PAID,
'payment_id' => $paymentID,
]);
$payment->save();
if ($notification) {
$user->notify(new ConfirmPaymentNotification($payment));
}
event(new PaymentEvent($user, $payment, $shopProduct));
event(new UserUpdateCreditsEvent($user));
}
$MpResponse = "paid";
} else {
if ($status == "cancelled") {
$user = User::findOrFail($payment->user_id);
$payment->update([
'status' => PaymentStatus::CANCELED,
'payment_id' => $paymentID,
]);
$payment->save();
event(new PaymentEvent($user, $payment, $shopProduct));
$MpResponse = "cancelled";
} else {
$user = User::findOrFail($payment->user_id);
$payment->update([
'status' => PaymentStatus::PROCESSING,
'payment_id' => $paymentID,
]);
$payment->save();
event(new PaymentEvent($user, $payment, $shopProduct));
$MpResponse = "processing";
}
}
}
return $MpResponse;
}
}
34 changes: 34 additions & 0 deletions app/Extensions/PaymentGateways/MercadoPago/MercadoPagoSettings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace App\Extensions\PaymentGateways\MercadoPago;

use Spatie\LaravelSettings\Settings;

class MercadoPagoSettings extends Settings
{

public bool $enabled = false;
public ?string $access_token;

public static function group(): string
{
return 'mercadopago';
}

public static function getOptionInputData()
{
return [
'category_icon' => 'fas fa-dollar-sign',
'access_token' => [
'type' => 'string',
'label' => 'Access Token Key',
'description' => 'The Access Token of your Mercado Pago App',
],
'enabled' => [
'type' => 'boolean',
'label' => 'Enabled',
'description' => 'Enable or disable this payment gateway',
],
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

use Spatie\LaravelSettings\Migrations\SettingsMigration;

class CreateMercadoPagoSettings extends SettingsMigration
{
public function up(): void
{
$this->migrator->addEncrypted('mpago.access_token', null);
$this->migrator->add('mpago.enabled', false);
}

public function down(): void
{
$this->migrator->delete('mpago.access_token');
$this->migrator->delete('mpago.enabled');
}
}
18 changes: 18 additions & 0 deletions app/Extensions/PaymentGateways/MercadoPago/web_routes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

use Illuminate\Support\Facades\Route;
use App\Extensions\PaymentGateways\MercadoPago\MercadoPagoExtension;

Route::middleware(['web', 'auth'])->group(function () {
Route::get(
'payment/MercadoPagoChecker',
function () {
MercadoPagoExtension::Checker(request());
}
)->name('payment.MercadoPagoChecker');
});


Route::post('payment/MercadoPagoWebhook', function () {
MercadoPagoExtension::Webhook(request());
})->name('payment.MercadoPagoWebhook');
3 changes: 3 additions & 0 deletions config/permissions_web.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@
'settings.paypal.read',
'settings.paypal.write',

'settings.mercadopago.read',
'settings.mercadopago.write',

'settings.stripe.read',
'settings.stripe.write',

Expand Down
6 changes: 4 additions & 2 deletions themes/BlueInfinity/views/layouts/main.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ class="nav-link @if (Request::routeIs('ticket.*')) active @endif">
@endif

<!-- lol how do i make this shorter? -->
@canany(['settings.discord.read','settings.discord.write','settings.general.read','settings.general.write','settings.invoice.read','settings.invoice.write','settings.locale.read','settings.locale.write','settings.mail.read','settings.mail.write','settings.pterodactyl.read','settings.pterodactyl.write','settings.referral.read','settings.referral.write','settings.server.read','settings.server.write','settings.ticket.read','settings.ticket.write','settings.user.read','settings.user.write','settings.website.read','settings.website.write','settings.paypal.read','settings.paypal.write','settings.stripe.read','settings.stripe.write','settings.mollie.read','settings.mollie.write','admin.overview.read','admin.overview.sync','admin.ticket.read','admin.tickets.write','admin.ticket_blacklist.read','admin.ticket_blacklist.write','admin.roles.read','admin.roles.write','admin.api.read','admin.api.write'])
@canany(['settings.discord.read','settings.discord.write','settings.general.read','settings.general.write','settings.invoice.read','settings.invoice.write','settings.locale.read','settings.locale.write','settings.mail.read','settings.mail.write','settings.pterodactyl.read','settings.pterodactyl.write','settings.referral.read','settings.referral.write','settings.server.read','settings.server.write','settings.ticket.read','settings.ticket.write','settings.user.read','settings.user.write','settings.website.read','settings.website.write','settings.paypal.read','settings.paypal.write','settings.stripe.read','settings.stripe.write','settings.mollie.read','settings.mollie.write','settings.mercadopago.read','settings.mercadopago.write','admin.overview.read','admin.overview.sync','admin.ticket.read','admin.tickets.write','admin.ticket_blacklist.read','admin.ticket_blacklist.write','admin.roles.read','admin.roles.write','admin.api.read','admin.api.write'])
<li class="nav-header">{{ __('Administration') }}</li>
@endcanany

Expand Down Expand Up @@ -329,7 +329,9 @@ class="nav-link @if (Request::routeIs('admin.roles.*')) active @endif">
'settings.stripe.read',
'settings.stripe.write',
'settings.mollie.read',
'settings.mollie.write',])
'settings.mollie.write',
'settings.mercadopago.read',
'settings.mercadopago.write',])
<li class="nav-item">
<a href="{{ route('admin.settings.index') }}"
class="nav-link @if (Request::routeIs('admin.settings.*')) active @endif">
Expand Down
6 changes: 4 additions & 2 deletions themes/default/views/layouts/main.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ class="nav-link @if (Request::routeIs('ticket.*')) active @endif">
@endif

<!-- lol how do i make this shorter? -->
@canany(['settings.discord.read','settings.discord.write','settings.general.read','settings.general.write','settings.invoice.read','settings.invoice.write','settings.locale.read','settings.locale.write','settings.mail.read','settings.mail.write','settings.pterodactyl.read','settings.pterodactyl.write','settings.referral.read','settings.referral.write','settings.server.read','settings.server.write','settings.ticket.read','settings.ticket.write','settings.user.read','settings.user.write','settings.website.read','settings.website.write','settings.paypal.read','settings.paypal.write','settings.stripe.read','settings.stripe.write','settings.mollie.read','settings.mollie.write','admin.overview.read','admin.overview.sync','admin.ticket.read','admin.tickets.write','admin.ticket_blacklist.read','admin.ticket_blacklist.write','admin.roles.read','admin.roles.write','admin.api.read','admin.api.write'])
@canany(['settings.discord.read','settings.discord.write','settings.general.read','settings.general.write','settings.invoice.read','settings.invoice.write','settings.locale.read','settings.locale.write','settings.mail.read','settings.mail.write','settings.pterodactyl.read','settings.pterodactyl.write','settings.referral.read','settings.referral.write','settings.server.read','settings.server.write','settings.ticket.read','settings.ticket.write','settings.user.read','settings.user.write','settings.website.read','settings.website.write','settings.paypal.read','settings.paypal.write','settings.stripe.read','settings.stripe.write','settings.mollie.read','settings.mollie.write','settings.mercadopago.read','settings.mercadopago.write','admin.overview.read','admin.overview.sync','admin.ticket.read','admin.tickets.write','admin.ticket_blacklist.read','admin.ticket_blacklist.write','admin.roles.read','admin.roles.write','admin.api.read','admin.api.write'])
<li class="nav-header">{{ __('Administration') }}</li>
@endcanany

Expand Down Expand Up @@ -329,7 +329,9 @@ class="nav-link @if (Request::routeIs('admin.roles.*')) active @endif">
'settings.stripe.read',
'settings.stripe.write',
'settings.mollie.read',
'settings.mollie.write',])
'settings.mollie.write',
'settings.mercadopago.read',
'settings.mercadopago.write',])
<li class="nav-item">
<a href="{{ route('admin.settings.index') }}"
class="nav-link @if (Request::routeIs('admin.settings.*')) active @endif">
Expand Down