diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php index ce561f31bf0c..e98e7f274d72 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -156,6 +156,7 @@ class CompanyGateway extends BaseModel '80af24a6a691230bbec33e930ab40666' => 323, 'vpyfbmdrkqcicpkjqdusgjfluebftuva' => 324, //BTPay '91be24c7b792230bced33e930ac61676' => 325, + 'wbhf02us6owgo7p4nfjd0ymssdshks4d' => 326, //Blockonomics ]; protected $touches = []; diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 43bbfe8fa719..70fff30a46ad 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -107,7 +107,9 @@ class Gateway extends StaticModel $link = 'https://docs.btcpayserver.org/InvoiceNinja/'; } elseif ($this->id == 63) { $link = 'https://rotessa.com'; - } + } elseif ($this->id == 64) { + $link = 'https://blockonomics.co'; + } return $link; } @@ -226,8 +228,8 @@ class Gateway extends StaticModel return [ GatewayType::CRYPTO => ['refund' => true, 'token_billing' => false, 'webhooks' => ['confirmed', 'paid_out', 'failed', 'fulfilled']], ]; //BTCPay - case 63: - return [ + case 63: + return [ GatewayType::BANK_TRANSFER => [ 'refund' => false, 'token_billing' => true, @@ -235,6 +237,10 @@ class Gateway extends StaticModel ], GatewayType::ACSS => ['refund' => false, 'token_billing' => true, 'webhooks' => []] ]; // Rotessa + case 64: + return [ + GatewayType::CRYPTO => ['refund' => true, '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 10eb1f2629a8..5f826985af84 100644 --- a/app/Models/SystemLog.php +++ b/app/Models/SystemLog.php @@ -153,7 +153,9 @@ class SystemLog extends Model public const TYPE_BTC_PAY = 324; public const TYPE_ROTESSA = 325; - + + public const TYPE_BLOCKONOMICS = 326; + public const TYPE_QUOTA_EXCEEDED = 400; public const TYPE_UPSTREAM_FAILURE = 401; diff --git a/app/PaymentDrivers/Blockonomics/Blockonomics.php b/app/PaymentDrivers/Blockonomics/Blockonomics.php new file mode 100644 index 000000000000..37e1ef050c16 --- /dev/null +++ b/app/PaymentDrivers/Blockonomics/Blockonomics.php @@ -0,0 +1,149 @@ +driver_class = $driver_class; + $this->driver_class->init(); + } + + 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.blockonomics.pay', $data); + } + + public function paymentResponse(PaymentResponseRequest $request) + { + + $request->validate([ + 'payment_hash' => ['required'], + 'amount' => ['required'], + 'currency' => ['required'], + ]); + + $drv = $this->driver_class; + if ( + strlen($drv->blockonomics_url) < 1 + || strlen($drv->api_key) < 1 + || strlen($drv->store_id) < 1 + || strlen($drv->webhook_secret) < 1 + ) { + throw new PaymentFailed('Blockonomics is not well configured'); + } + if (!filter_var($this->driver_class->blockonomics_url, FILTER_VALIDATE_URL)) { + throw new PaymentFailed('Wrong format for Blockonomics Url'); + } + + try { + $_invoice = collect($drv->payment_hash->data->invoices)->first(); + $cli = $drv->client; + + $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", + 'InvoiceNinjaPaymentHash' => $drv->payment_hash->hash + ]; + + + $urlRedirect = redirect()->route('client.invoice.show', ['invoice' => $_invoice->invoice_id])->getTargetUrl(); + + $rep = $client->createInvoice( + $drv->store_id, + $request->currency, + $_invoice->invoice_number, + $cli->present()->email(), + $metaData, + $checkoutOptions + ); + + return redirect($rep->getCheckoutLink()); + } catch (\Throwable $e) { + PaymentFailureMailer::dispatch($drv->client, $drv->payment_hash->data, $drv->client->company, $request->amount); + throw new PaymentFailed('Error during Blockonomics payment : ' . $e->getMessage()); + } + } + + public function refund(Payment $payment, $amount) + { + try { + if ($amount == $payment->amount) { + $refundVariant = "Fiat"; + $refundPaymentMethod = "BTC"; + $refundDescription = "Full refund"; + $refundCustomCurrency = null; + $refundCustomAmount = null; + } else { + $refundVariant = "Custom"; + $refundPaymentMethod = ""; + $refundDescription = "Partial refund"; + $refundCustomCurrency = $payment->currency; + $refundCustomAmount = $amount; + } + App::setLocale($payment->company->getLocale()); + + $email_object = new EmailObject(); + $email_object->subject = ctrans('texts.blockonomics_refund_subject'); + $email_object->body = ctrans('texts.blockonomics_refund_body') . '
' . $refund->getViewLink(); + $email_object->text_body = ctrans('texts.blockonomics_refund_body') . '\n' . $refund->getViewLink(); + $email_object->company_key = $payment->company->company_key; + $email_object->html_template = 'email.template.generic'; + $email_object->to = [new Address($payment->client->present()->email(), $payment->client->present()->name())]; + $email_object->email_template_body = 'blockonomics_refund_subject'; + $email_object->email_template_subject = 'blockonomics_refund_body'; + + Email::dispatch($email_object, $payment->company); + + $data = [ + 'transaction_reference' => $refund->getId(), + 'transaction_response' => json_encode($refund), + 'success' => true, + 'description' => "Please follow this link to claim your refund: " . $refund->getViewLink(), + 'code' => 202, + ]; + + return $data; + } catch (\Throwable $e) { + throw new PaymentFailed('Error during Blockonomics refund : ' . $e->getMessage()); + } + } +} diff --git a/app/PaymentDrivers/BlockonomicsPaymentDriver.php b/app/PaymentDrivers/BlockonomicsPaymentDriver.php new file mode 100644 index 000000000000..170d0c134998 --- /dev/null +++ b/app/PaymentDrivers/BlockonomicsPaymentDriver.php @@ -0,0 +1,174 @@ + 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 $blockonomics_url = ""; + public $api_key = ""; + public $store_id = ""; + public $webhook_secret = ""; + public $blockonomics; + + + public function init() + { + $this->blockonomics_url = $this->company_gateway->getConfigField('blockonomicsUrl'); + $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 processPaymentResponse($request) + { + return $this->payment_method->paymentResponse($request); + } + + public function processWebhookRequest() + { + $webhook_payload = file_get_contents('php://input'); + + /** @var \stdClass $blockonomicsRep */ + $blockonomicsRep = json_decode($webhook_payload); + if ($blockonomicsRep == null) { + throw new PaymentFailed('Empty data'); + } + if (true === empty($blockonomicsRep->invoiceId)) { + throw new PaymentFailed( + 'Invalid payment notification- did not receive invoice ID.' + ); + } + if ( + str_starts_with($blockonomicsRep->invoiceId, "__test__") + || $blockonomicsRep->type == "InvoiceProcessing" + || $blockonomicsRep->type == "InvoiceCreated" + ) { + return; + } + + $sig = ''; + $headers = getallheaders(); + foreach ($headers as $key => $value) { + if (strtolower($key) === 'blockonomics-sig') { + $sig = $value; + } + } + + $this->init(); + $webhookClient = new Webhook($this->blockonomics_url, $this->api_key); + + if (!$webhookClient->isIncomingWebhookRequestValid($webhook_payload, $sig, $this->webhook_secret)) { + throw new \RuntimeException( + 'Invalid payment notification message received - signature did not match.' + ); + } + + $this->setPaymentMethod(GatewayType::CRYPTO); + $this->payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$blockonomicsRep->metadata->InvoiceNinjaPaymentHash])->firstOrFail(); + $StatusId = Payment::STATUS_PENDING; + if ($this->payment_hash->payment_id == null) { + + $_invoice = Invoice::with('client')->withTrashed()->find($this->payment_hash->fee_invoice_id); + + $this->client = $_invoice->client; + + $dataPayment = [ + 'payment_method' => $this->payment_method, + 'payment_type' => PaymentType::CRYPTO, + 'amount' => $_invoice->amount, + 'gateway_type_id' => GatewayType::CRYPTO, + 'transaction_reference' => $blockonomicsRep->invoiceId + ]; + $payment = $this->createPayment($dataPayment, $StatusId); + } else { + /** @var \App\Models\Payment $payment */ + $payment = Payment::withTrashed()->find($this->payment_hash->payment_id); + $StatusId = $payment->status_id; + } + switch ($blockonomicsRep->type) { + case "InvoiceExpired": + $StatusId = Payment::STATUS_CANCELLED; + break; + case "InvoiceInvalid": + $StatusId = Payment::STATUS_FAILED; + break; + case "InvoiceSettled": + $StatusId = Payment::STATUS_COMPLETED; + break; + } + if ($payment->status_id != $StatusId) { + $payment->status_id = $StatusId; + $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/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..dcca0eeaffb6 --- /dev/null +++ b/database/migrations/2024_08_27_230111_blockonomics_gateway.php @@ -0,0 +1,47 @@ +blockonomicsUrl = ""; + $fields->apiKey = ""; + $fields->storeId = ""; + $fields->webhookSecret = ""; + + $gateway = new Gateway; + $gateway->id = 64; + $gateway->name = 'Blockonomics'; + $gateway->key = 'wbhf02us6owgo7p4nfjd0ymssdshks4d'; + $gateway->provider = 'Blockonomics'; + $gateway->is_offsite = true; + $gateway->fields = \json_encode($fields); + + + $gateway->visible = 1; + $gateway->site_url = 'https://blockonomics.co'; + $gateway->default_gateway_type_id = GatewayType::CRYPTO; + $gateway->save(); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; \ No newline at end of file diff --git a/database/seeders/PaymentLibrariesSeeder.php b/database/seeders/PaymentLibrariesSeeder.php index d85f47b782b3..fb61aebdd175 100644 --- a/database/seeders/PaymentLibrariesSeeder.php +++ b/database/seeders/PaymentLibrariesSeeder.php @@ -89,6 +89,7 @@ class PaymentLibrariesSeeder extends Seeder ['id' => 61, 'name' => 'PayPal Platform', 'provider' => 'PayPal_PPCP', 'key' => '80af24a6a691230bbec33e930ab40666', 'fields' => '{"testMode":false}'], ['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":""}'], + ['id' => 64, 'name' => 'Blockonomics', 'provider' => 'Blockonomics', 'key' => 'wbhf02us6owgo7p4nfjd0ymssdshks4d', 'fields' => '{"blockonomicsUrl":"", "apiKey":"", "storeId":"", "webhookSecret":""}'], ]; foreach ($gateways as $gateway) { @@ -105,7 +106,7 @@ class PaymentLibrariesSeeder extends Seeder Gateway::query()->update(['visible' => 0]); - Gateway::whereIn('id', [1, 3, 7, 11, 15, 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])->update(['visible' => 1]); if (Ninja::isHosted()) { Gateway::whereIn('id', [20, 49])->update(['visible' => 0]);