diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 0faadebd6819..e62221d8b02e 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -1,4 +1,5 @@ id == 1) { $link = 'http://reseller.authorize.net/application/?id=5560364'; - } elseif (in_array($this->id, [15,60,61])) { + } elseif (in_array($this->id, [15, 60, 61])) { $link = 'https://www.paypal.com/us/cgi-bin/webscr?cmd=_login-api-run'; } elseif ($this->id == 24) { $link = 'https://www.2checkout.com/referral?r=2c37ac2298'; @@ -102,6 +103,8 @@ class Gateway extends StaticModel $link = 'https://dashboard.stripe.com/account/apikeys'; } elseif ($this->id == 59) { $link = 'https://www.forte.net/'; + } elseif ($this->id == 62) { + $link = 'https://docs.btcpayserver.org'; } return $link; @@ -137,8 +140,8 @@ class Gateway extends StaticModel case 56: return [ GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => ['payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'customer.source.updated','payment_intent.processing', 'payment_intent.payment_failed', 'charge.failed']], - GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing','payment_intent.succeeded','payment_intent.partially_funded', 'payment_intent.payment_failed']], + GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'customer.source.updated', 'payment_intent.processing', 'payment_intent.payment_failed', 'charge.failed']], + GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => false, 'webhooks' => ['payment_intent.processing', 'payment_intent.succeeded', 'payment_intent.partially_funded', 'payment_intent.payment_failed']], GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false], GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false], GatewayType::BACS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.processing', 'payment_intent.succeeded', 'mandate.updated', 'payment_intent.payment_failed']], @@ -152,7 +155,7 @@ class Gateway extends StaticModel GatewayType::BECS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::IDEAL => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], GatewayType::ACSS => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', 'payment_intent.succeeded', 'payment_intent.payment_failed']], - GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed', ]], + GatewayType::FPX => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded', 'charge.failed',]], ]; case 39: return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true, 'webhooks' => [' ']]]; //Checkout @@ -175,10 +178,10 @@ class Gateway extends StaticModel ]; case 52: return [ - GatewayType::BANK_TRANSFER => ['refund' => false, 'token_billing' => true, 'webhooks' => ['confirmed','paid_out','failed','fulfilled']], // GoCardless - GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => true, 'webhooks' => ['confirmed','paid_out','failed','fulfilled']], - GatewayType::SEPA => ['refund' => false, 'token_billing' => true, 'webhooks' => ['confirmed','paid_out','failed','fulfilled']], - GatewayType::INSTANT_BANK_PAY => ['refund' => false, 'token_billing' => true, 'webhooks' => ['confirmed','paid_out','failed','fulfilled']], + GatewayType::BANK_TRANSFER => ['refund' => false, 'token_billing' => true, 'webhooks' => ['confirmed', 'paid_out', 'failed', 'fulfilled']], // GoCardless + GatewayType::DIRECT_DEBIT => ['refund' => false, 'token_billing' => true, 'webhooks' => ['confirmed', 'paid_out', 'failed', 'fulfilled']], + GatewayType::SEPA => ['refund' => false, 'token_billing' => true, 'webhooks' => ['confirmed', 'paid_out', 'failed', 'fulfilled']], + GatewayType::INSTANT_BANK_PAY => ['refund' => false, 'token_billing' => true, 'webhooks' => ['confirmed', 'paid_out', 'failed', 'fulfilled']], ]; case 58: return [ @@ -217,6 +220,10 @@ class Gateway extends StaticModel // GatewayType::PRZELEWY24 => ['refund' => false, 'token_billing' => false], // GatewayType::SOFORT => ['refund' => false, 'token_billing' => false], ]; //Paypal PPCP + case 62: + return [ + GatewayType::CRYPTO => ['refund' => true, 'token_billing' => false, 'webhooks' => ['confirmed', 'paid_out', 'failed', 'fulfilled']], + ]; //BTCPay default: return []; } diff --git a/app/PaymentDrivers/BTCPay/BTCPay.php b/app/PaymentDrivers/BTCPay/BTCPay.php new file mode 100644 index 000000000000..708018b30a46 --- /dev/null +++ b/app/PaymentDrivers/BTCPay/BTCPay.php @@ -0,0 +1,147 @@ +driver_class = $driver_class; + $this->driver_class->init(); + } + + public function authorizeView($data) + { + } + + public function authorizeRequest($request) + { + } + public function authorizeResponse($request) + { + } + + public function paymentView($data) + { + $data['gateway'] = $this->driver_class; + $data['amount'] = $data['total']['amount_with_fee']; + $data['currency'] = $this->driver_class->client->getCurrencyCode(); + + return render('gateways.btcpay.pay', $data); + } + + public function paymentResponse(PaymentResponseRequest $request) + { + + $request->validate([ + 'payment_hash' => ['required'], + 'amount' => ['required'], + 'currency' => ['required'], + ]); + + $drv = $this->driver_class; + if ( + strlen($drv->btcpay_url) < 1 + || strlen($drv->api_key) < 1 + || strlen($drv->store_id) < 1 + || strlen($drv->webhook_secret) < 1 + ) { + throw new PaymentFailed('BTCPay is not well configured'); + } + if (!filter_var($this->driver_class->btcpay_url, FILTER_VALIDATE_URL)) { + throw new PaymentFailed('Wrong format for BTCPay Url'); + } + + try { + $_invoice = collect($drv->payment_hash->data->invoices)->first(); + $cli = $drv->client; + + $dataPayment = [ + 'payment_method' => $drv->payment_method, + 'payment_type' => PaymentType::CRYPTO, + 'amount' => $request->amount, + 'gateway_type_id' => GatewayType::CRYPTO, + 'transaction_reference' => 'xxx' + ]; + $payment = $drv->createPayment($dataPayment, \App\Models\Payment::STATUS_PENDING); + + $metaData = [ + 'buyerName' => $cli->name, + 'buyerAddress1' => $cli->address1, + 'buyerAddress2' => $cli->address2, + 'buyerCity' => $cli->city, + 'buyerState' => $cli->state, + 'buyerZip' => $cli->postal_code, + 'buyerCountry' => $cli->country_id, + 'buyerPhone' => $cli->phone, + 'itemDesc' => "From InvoiceNinja", + 'paymentID' => $payment->id + ]; + + + $urlRedirect = redirect()->route('client.payments.show', ['payment' => $payment->hashed_id])->getTargetUrl(); + $checkoutOptions = new \BTCPayServer\Client\InvoiceCheckoutOptions(); + $checkoutOptions->setRedirectURL($urlRedirect); + + $client = new \BTCPayServer\Client\Invoice($drv->btcpay_url, $drv->api_key); + $rep = $client->createInvoice( + $drv->store_id, + $request->currency, + \BTCPayServer\Util\PreciseNumber::parseString($request->amount), + $_invoice->invoice_number, + $cli->present()->email(), + $metaData, + $checkoutOptions + ); + $payment->transaction_reference = $rep->getId(); + $payment->save(); + + return redirect($rep->getCheckoutLink()); + } catch (\Throwable $e) { + throw new PaymentFailed('Error during BTCPay payment : ' . $e->getMessage()); + } + } + + public function refund(Payment $payment, $amount) + { + try { + $invoice = $payment->invoices()->first(); + $isPartialRefund = ($amount < $payment->amount); + + $client = new \BTCPayServer\Client\Invoice($this->driver_class->btcpay_url, $this->driver_class->api_key); + $refund = $client->refundInvoice($this->driver_class->store_id, $payment->transaction_reference); + + /* $data = []; + $data['InvoiceNumber'] = $invoice->number; + $data['isPartialRefund'] = $isPartialRefund; + $data['BTCPayLink'] = $refund->getViewLink();*/ + + return $refund->getViewLink(); + } catch (\Throwable $e) { + throw new PaymentFailed('Error during BTCPay refund : ' . $e->getMessage()); + } + } +} diff --git a/app/PaymentDrivers/BTCPayPaymentDriver.php b/app/PaymentDrivers/BTCPayPaymentDriver.php new file mode 100644 index 000000000000..4ede193c149b --- /dev/null +++ b/app/PaymentDrivers/BTCPayPaymentDriver.php @@ -0,0 +1,138 @@ + BTCPay::class, //maps GatewayType => Implementation class + ]; + + const SYSTEM_LOG_TYPE = SystemLog::TYPE_CHECKOUT; //define a constant for your gateway ie TYPE_YOUR_CUSTOM_GATEWAY - set the const in the SystemLog model + + public $btcpay_url = ""; + public $api_key = ""; + public $store_id = ""; + public $webhook_secret = ""; + public $btcpay; + + + public function init() + { + $this->btcpay_url = $this->company_gateway->getConfigField('btcpayUrl'); + $this->api_key = $this->company_gateway->getConfigField('apiKey'); + $this->store_id = $this->company_gateway->getConfigField('storeId'); + $this->webhook_secret = $this->company_gateway->getConfigField('webhookSecret'); + return $this; /* This is where you boot the gateway with your auth credentials*/ + } + + /* Returns an array of gateway types for the payment gateway */ + public function gatewayTypes(): array + { + $types = []; + + $types[] = GatewayType::CRYPTO; + + return $types; + } + + public function setPaymentMethod($payment_method_id) + { + $class = self::$methods[$payment_method_id]; + $this->payment_method = new $class($this); + return $this; + } + + public function processPaymentView(array $data) + { + return $this->payment_method->paymentView($data); //this is your custom implementation from here + } + + public function processWebhookRequest() + { + $webhook_payload = file_get_contents('php://input'); + //file_put_contents("/home/claude/invoiceninja/storage/my.log", $webhook_payload); + + $btcpayRep = json_decode($webhook_payload); + if ($btcpayRep == null) { + throw new PaymentFailed('Empty data'); + } + if (true === empty($btcpayRep->invoiceId)) { + throw new PaymentFailed( + 'Invalid BTCPayServer payment notification- did not receive invoice ID.' + ); + } + if (str_starts_with($btcpayRep->invoiceId, "__test__") || $btcpayRep->type == "InvoiceCreated") { + return; + } + + $headers = getallheaders(); + foreach ($headers as $key => $value) { + if (strtolower($key) === 'btcpay-sig') { + $sig = $value; + } + } + + $this->init(); + $webhookClient = new Webhook($this->btcpay_url, $this->api_key); + + if (!$webhookClient->isIncomingWebhookRequestValid($webhook_payload, $sig, $this->webhook_secret)) { + throw new \RuntimeException( + 'Invalid BTCPayServer payment notification message received - signature did not match.' + ); + } + + /** @var \App\Models\Payment $payment **/ + $payment = Payment::find($btcpayRep->metafata->paymentID); + switch ($btcpayRep->type) { + case "InvoiceExpired": + $payment->status_id = Payment::STATUS_CANCELLED; + break; + case "InvoiceInvalid": + $payment->status_id = Payment::STATUS_FAILED; + break; + case "InvoiceSettled": + $payment->status_id = Payment::STATUS_COMPLETED; + break; + } + $payment->save(); + } + + + public function refund(Payment $payment, $amount, $return_client_response = false) + { + $this->setPaymentMethod(GatewayType::CRYPTO); + return $this->payment_method->refund($payment, $amount); //this is your custom implementation from here + } +} diff --git a/composer.json b/composer.json index cd273d7f6ea7..5e8f755d91e7 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ "bacon/bacon-qr-code": "^2.0", "beganovich/snappdf": "^4", "braintree/braintree_php": "^6.0", + "btcpayserver/btcpayserver-greenfield-php": "^2.6", "checkout/checkout-sdk-php": "^3.0", "invoiceninja/ubl_invoice": "^2", "doctrine/dbal": "^3.0", diff --git a/composer.lock b/composer.lock index b148de831539..6b497a2a6f50 100644 --- a/composer.lock +++ b/composer.lock @@ -1687,6 +1687,60 @@ ], "time": "2023-01-15T23:15:59+00:00" }, + { + "name": "btcpayserver/btcpayserver-greenfield-php", + "version": "v2.6.0", + "source": { + "type": "git", + "url": "https://github.com/btcpayserver/btcpayserver-greenfield-php.git", + "reference": "c115b0415719b9fe6e35d5df5f291646d4af2240" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/btcpayserver/btcpayserver-greenfield-php/zipball/c115b0415719b9fe6e35d5df5f291646d4af2240", + "reference": "c115b0415719b9fe6e35d5df5f291646d4af2240", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.0", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.8", + "vlucas/phpdotenv": "^5.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "BTCPayServer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Wouter Samaey", + "email": "wouter.samaey@storefront.be" + }, + { + "name": "Andreas Tasch", + "email": "andy.tasch@gmail.com" + } + ], + "description": "BTCPay Server Greenfield API PHP client library.", + "support": { + "issues": "https://github.com/btcpayserver/btcpayserver-greenfield-php/issues", + "source": "https://github.com/btcpayserver/btcpayserver-greenfield-php/tree/v2.6.0" + }, + "time": "2024-04-25T09:19:49+00:00" + }, { "name": "carbonphp/carbon-doctrine-types", "version": "2.1.0", diff --git a/database/migrations/2024_05_03_145535_btcpay_gateway.php b/database/migrations/2024_05_03_145535_btcpay_gateway.php new file mode 100644 index 000000000000..5f952ce4ff76 --- /dev/null +++ b/database/migrations/2024_05_03_145535_btcpay_gateway.php @@ -0,0 +1,42 @@ +name = 'BTCPay'; + $gateway->key = 'vpyfbmdrkqcicpkjqdusgjfluebftuva'; + $gateway->provider = 'BTCPay'; + $gateway->is_offsite = true; + + $btcpayFieds = new \stdClass; + $btcpayFieds->btcpayUrl = ""; + $btcpayFieds->apiKey = ""; + $btcpayFieds->storeId = ""; + $btcpayFieds->webhookSecret = ""; + $gateway->fields = \json_encode($btcpayFieds); + + + $gateway->visible = true; + $gateway->site_url = 'https://btcpayserver.org'; + $gateway->default_gateway_type_id = GatewayType::CRYPTO; + $gateway->save(); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/resources/views/portal/ninja2020/gateways/btcpay/pay.blade.php b/resources/views/portal/ninja2020/gateways/btcpay/pay.blade.php new file mode 100644 index 000000000000..a53ac383fb09 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/btcpay/pay.blade.php @@ -0,0 +1,28 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.payment_type_Crypto'), 'card_title' => ctrans('texts.payment_type_Crypto')]) + +@section('gateway_content') +
+ + @include('portal.ninja2020.gateways.includes.payment_details') + + + + @include('portal.ninja2020.gateways.includes.pay_now') +@endsection + +@push('footer') + +@endpush