diff --git a/app/Http/Controllers/Gateways/BlockonomicsController.php b/app/Http/Controllers/Gateways/BlockonomicsController.php new file mode 100644 index 000000000000..4cdbfd5ed80f --- /dev/null +++ b/app/Http/Controllers/Gateways/BlockonomicsController.php @@ -0,0 +1,58 @@ +query('currency'); + $response = Http::get("https://www.blockonomics.co/api/price?currency={$currency}"); + + if ($response->successful()) { + return response()->json(['price' => $response->json('price')]); + } + + return response()->json(['error' => 'Unable to fetch BTC price'], 500); + } + + public function getQRCode(Request $request) + { + $qr_string = $request->query('qr_string'); + $svg = $this->getPaymentQrCodeRaw($qr_string); + return response($svg)->header('Content-Type', 'image/svg+xml'); + } + + private function getPaymentQrCodeRaw($qr_string) + { + + $renderer = new ImageRenderer( + new RendererStyle(150, margin: 0), + new SvgImageBackEnd() + ); + $writer = new Writer($renderer); + + $qr = $writer->writeString($qr_string, 'utf-8'); + + return $qr; + + } +} \ No newline at end of file diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php index 73dbae6617d7..e2d939db2515 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -156,9 +156,10 @@ class CompanyGateway extends BaseModel 'b9886f9257f0c6ee7c302f1c74475f6c' => 321, //GoCardless 'hxd6gwg3ekb9tb3v9lptgx1mqyg69zu9' => 322, '80af24a6a691230bbec33e930ab40666' => 323, - 'vpyfbmdrkqcicpkjqdusgjfluebftuva' => 324, //BTPay + 'vpyfbmdrkqcicpkjqdusgjfluebftuva' => 324, //BTCPay '91be24c7b792230bced33e930ac61676' => 325, - 'b67581d804dbad1743b61c57285142ad' => 326, //Powerboard + 'wbhf02us6owgo7p4nfjd0ymssdshks4d' => 326, //Blockonomics + 'b67581d804dbad1743b61c57285142ad' => 327, //Powerboard ]; protected $touches = []; diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index d1276d931348..ab62c3200f32 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -25,7 +25,7 @@ namespace App\Models; * @property bool $is_offsite * @property bool $is_secure * @property object|null|string $fields - * @property string $default_gateway_type_id + * @property string|int $default_gateway_type_id * @property int|null $created_at * @property int|null $updated_at * @property-read mixed $options @@ -106,7 +106,9 @@ class Gateway extends StaticModel } elseif ($this->id == 62) { $link = 'https://docs.btcpayserver.org/InvoiceNinja/'; } elseif ($this->id == 63) { - $link = 'https://rotessa.com'; + $link = 'https://rotessa.com'; + } elseif ($this->id == 64) { + $link = 'https://blockonomics.co'; } return $link; @@ -228,17 +230,21 @@ class Gateway extends StaticModel ]; //BTCPay case 63: return [ - GatewayType::BANK_TRANSFER => [ - 'refund' => false, - 'token_billing' => true, - 'webhooks' => [], - ], - GatewayType::ACSS => ['refund' => false, 'token_billing' => true, 'webhooks' => []] - ]; // Rotessa + GatewayType::BANK_TRANSFER => [ + 'refund' => false, + 'token_billing' => true, + 'webhooks' => [], + ], + GatewayType::ACSS => ['refund' => false, 'token_billing' => true, 'webhooks' => []] + ]; // Rotessa case 64: //b67581d804dbad1743b61c57285142ad - powerboard return [ GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], ]; + case 65: + return [ + GatewayType::CRYPTO => ['refund' => false, 'token_billing' => false, 'webhooks' => ['confirmed', 'paid_out', 'failed', 'fulfilled']], + ]; //Blockonomics default: return []; } diff --git a/app/Models/PaymentType.php b/app/Models/PaymentType.php index 9565c8cd9494..db772ccb8f93 100644 --- a/app/Models/PaymentType.php +++ b/app/Models/PaymentType.php @@ -81,6 +81,7 @@ class PaymentType extends StaticModel public const STRIPE_BANK_TRANSFER = 50; public const CASH_APP = 51; public const PAY_LATER = 52; + public const BLOCKONOMICS = 64; public array $type_names = [ self::BANK_TRANSFER => 'payment_type_Bank Transfer', @@ -129,6 +130,7 @@ class PaymentType extends StaticModel self::CASH_APP => 'payment_type_Cash App', self::VENMO => 'payment_type_Venmo', self::PAY_LATER => 'payment_type_Pay Later', + self::BLOCKONOMICS => 'payment_type_Blockonomics', ]; public static function parseCardType($cardName) diff --git a/app/Models/SystemLog.php b/app/Models/SystemLog.php index acc05c3246db..0f833fa3f32e 100644 --- a/app/Models/SystemLog.php +++ b/app/Models/SystemLog.php @@ -155,8 +155,10 @@ class SystemLog extends Model public const TYPE_BTC_PAY = 324; public const TYPE_ROTESSA = 325; - - public const TYPE_POWERBOARD = 326; + + public const TYPE_BLOCKONOMICS = 326; + + public const TYPE_POWERBOARD = 327; public const TYPE_QUOTA_EXCEEDED = 400; diff --git a/app/PaymentDrivers/Blockonomics/Blockonomics.php b/app/PaymentDrivers/Blockonomics/Blockonomics.php new file mode 100644 index 000000000000..b2e5d8888703 --- /dev/null +++ b/app/PaymentDrivers/Blockonomics/Blockonomics.php @@ -0,0 +1,138 @@ +blockonomics->api_key; + // $params = config('ninja.environment') == 'development' ? '?reset=1' : ''; + $url = 'https://www.blockonomics.co/api/new_address'; + + $r = Http::withToken($api_key) + ->post($url, []); + + if($r->successful()) + return $r->object()->address ?? 'Something went wrong'; + + return $r->object()->message ?? 'Something went wrong'; + + } + + public function getBTCPrice() + { + + $r = Http::get('https://www.blockonomics.co/api/price', ['currency' => $this->blockonomics->client->getCurrencyCode()]); + + return $r->successful() ? $r->object()->price : 'Something went wrong'; + + } + + public function paymentView($data) + { + $btc_price = $this->getBTCPrice(); + $btc_address = $this->getBTCAddress(); + $fiat_amount = $data['total']['amount_with_fee']; + $btc_amount = $fiat_amount / $btc_price; + $_invoice = collect($this->blockonomics->payment_hash->data->invoices)->first(); + $data['gateway'] = $this->blockonomics; + $data['company_gateway_id'] = $this->blockonomics->getCompanyGatewayId(); + $data['amount'] = $fiat_amount; + $data['currency'] = $this->blockonomics->client->getCurrencyCode(); + $data['btc_amount'] = number_format($btc_amount, 10, '.', ''); + $data['btc_address'] = $btc_address; + $data['btc_price'] = $btc_price; + $data['invoice_number'] = $_invoice->invoice_number; + return render('gateways.blockonomics.pay', $data); + } + + public function paymentResponse(PaymentResponseRequest $request) + { + $request->validate([ + 'payment_hash' => ['required'], + 'amount' => ['required'], + 'currency' => ['required'], + 'txid' => ['required'], + 'payment_method_id' => ['required'], + ]); + + try { + $data = []; + $fiat_amount = round(($request->btc_price * $request->btc_amount), 2); + $data['amount'] = $fiat_amount; + $data['currency'] = $request->currency; + $data['payment_method_id'] = $request->payment_method_id; + $data['payment_type'] = PaymentType::CRYPTO; + $data['gateway_type_id'] = GatewayType::CRYPTO; + $data['transaction_reference'] = $request->txid; + + $statusId = Payment::STATUS_PENDING; + $payment = $this->blockonomics->createPayment($data, $statusId); + + SystemLogger::dispatch( + ['response' => $payment, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_BLOCKONOMICS, + $this->blockonomics->client, + $this->blockonomics->client->company, + ); + + return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]); + + } catch (\Throwable $e) { + $blockonomics = $this->blockonomics; + PaymentFailureMailer::dispatch($blockonomics->client, $blockonomics->payment_hash->data, $blockonomics->client->company, $request->amount); + throw new PaymentFailed('Error during Blockonomics payment : ' . $e->getMessage()); + } + } + + // Not supported yet + public function refund(Payment $payment, $amount) + { + return; + } +} diff --git a/app/PaymentDrivers/BlockonomicsPaymentDriver.php b/app/PaymentDrivers/BlockonomicsPaymentDriver.php new file mode 100644 index 000000000000..c49256727248 --- /dev/null +++ b/app/PaymentDrivers/BlockonomicsPaymentDriver.php @@ -0,0 +1,151 @@ + Blockonomics::class, //maps GatewayType => Implementation class + ]; + + public 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 $BASE_URL = 'https://www.blockonomics.co'; + public $NEW_ADDRESS_URL = 'https://www.blockonomics.co/api/new_address'; + public $PRICE_URL = 'https://www.blockonomics.co/api/price'; + + public $api_key; + public $callback_secret; + + public function init() + { + $this->api_key = $this->company_gateway->getConfigField('apiKey'); + $this->callback_secret = $this->company_gateway->getConfigField('callbackSecret'); + 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) + { + $this->init(); + + return $this->payment_method->paymentView($data); //this is your custom implementation from here + } + + public function processPaymentResponse($request) + { + + $this->init(); + + return $this->payment_method->paymentResponse($request); + } + + public function processWebhookRequest(PaymentWebhookRequest $request) + { + + $company = $request->getCompany(); + + $url_callback_secret = $request->secret; + $db_callback_secret = $this->company_gateway->getConfigField('callbackSecret'); + + if ($url_callback_secret != $db_callback_secret) { + throw new PaymentFailed('Secret does not match'); + } + + $txid = $request->txid; + $value = $request->value; + $status = $request->status; + $addr = $request->addr; + + $payment = Payment::query() + ->where('company_id', $company->id) + ->where('transaction_reference', $txid) + ->firstOrFail(); + + if (!$payment) { + return response()->json([], 200); + // TODO: Implement logic to create new payment in case user sends payment to the address after closing the payment page + } + + $statusId = Payment::STATUS_PENDING; + + switch ($status) { + case 0: + $statusId = Payment::STATUS_PENDING; + break; + case 1: + $statusId = Payment::STATUS_PENDING; + break; + case 2: + $statusId = Payment::STATUS_COMPLETED; + break; + } + + if($payment->status_id == $statusId) { + return response()->json([], 200); + } else { + $payment->status_id = $statusId; + $payment->save(); + + return response()->json([], 200); + } + } + + + 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/database/migrations/2024_08_27_230111_blockonomics_gateway.php b/database/migrations/2024_08_27_230111_blockonomics_gateway.php new file mode 100644 index 000000000000..14d13aa1a2a4 --- /dev/null +++ b/database/migrations/2024_08_27_230111_blockonomics_gateway.php @@ -0,0 +1,45 @@ +apiKey = ""; + $fields->callbackSecret = ""; + + $gateway = new Gateway; + $gateway->id = 65; + $gateway->name = 'Blockonomics'; + $gateway->key = 'wbhf02us6owgo7p4nfjd0ymssdshks4d'; + $gateway->provider = 'Blockonomics'; + $gateway->is_offsite = false; + $gateway->fields = \json_encode($fields); + + + $gateway->visible = true; + $gateway->site_url = 'https://blockonomics.co'; + $gateway->default_gateway_type_id = GatewayType::CRYPTO; + $gateway->save(); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/database/seeders/PaymentLibrariesSeeder.php b/database/seeders/PaymentLibrariesSeeder.php index 879b65dc8831..970ad5cecb2e 100644 --- a/database/seeders/PaymentLibrariesSeeder.php +++ b/database/seeders/PaymentLibrariesSeeder.php @@ -90,6 +90,7 @@ class PaymentLibrariesSeeder extends Seeder ['id' => 62, 'name' => 'BTCPay', 'provider' => 'BTCPay', 'key' => 'vpyfbmdrkqcicpkjqdusgjfluebftuva', 'fields' => '{"btcpayUrl":"", "apiKey":"", "storeId":"", "webhookSecret":""}'], ['id' => 63, 'name' => 'Rotessa', 'is_offsite' => false, 'sort_order' => 22, 'provider' => 'Rotessa', 'key' => '91be24c7b792230bced33e930ac61676', 'fields' => '{"apiKey":"", "testMode":false}'], ['id' => 64, 'name' => 'CBA PowerBoard', 'is_offsite' => false, 'sort_order' => 26, 'provider' => 'CBAPowerBoard', 'key' => 'b67581d804dbad1743b61c57285142ad', 'fields' => '{"publicKey":"", "secretKey":"", "testMode":false, "Threeds":true}'], + ['id' => 65, 'name' => 'Blockonomics', 'is_offsite' => false, 'sort_order' => 27, 'provider' => 'Blockonomics', 'key' => 'wbhf02us6owgo7p4nfjd0ymssdshks4d', 'fields' => '{"apiKey":"", "callbackSecret":""}'], ]; foreach ($gateways as $gateway) { @@ -106,7 +107,7 @@ class PaymentLibrariesSeeder extends Seeder Gateway::query()->update(['visible' => 0]); - Gateway::whereIn('id', [1, 3, 7, 11, 20, 39, 46, 55, 50, 57, 52, 58, 59, 60, 62, 63])->update(['visible' => 1]); + Gateway::whereIn('id', [1, 3, 7, 11, 15, 20, 39, 46, 55, 50, 57, 52, 58, 59, 60, 62, 63, 64, 65])->update(['visible' => 1]); if (Ninja::isHosted()) { Gateway::whereIn('id', [20, 49])->update(['visible' => 0]); diff --git a/public/build/manifest.json b/public/build/manifest.json index e3ebfcf7ed75..8672452a121d 100644 --- a/public/build/manifest.json +++ b/public/build/manifest.json @@ -364,7 +364,11 @@ "src": "resources/js/setup/setup.js" }, "resources/sass/app.scss": { +<<<<<<< HEAD "file": "assets/app-22043157.css", +======= + "file": "assets/app-61bfe239.css", +>>>>>>> blockonomics-driver "isEntry": true, "src": "resources/sass/app.scss" } diff --git a/resources/views/portal/ninja2020/gateways/blockonomics/pay.blade.php b/resources/views/portal/ninja2020/gateways/blockonomics/pay.blade.php new file mode 100644 index 000000000000..59e11dd368a1 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/blockonomics/pay.blade.php @@ -0,0 +1,322 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.payment_type_Crypto'), 'card_title' => ctrans('texts.payment_type_Crypto')]) + +@section('gateway_content') +
+