Compare commits

..

7 Commits
master ... 3.x

Author SHA1 Message Date
Andreas Tasch
f6bc1d5632 Minor fixes. 2023-01-25 15:24:49 +01:00
Andreas Tasch
f09c8cbc7c Bump version. 2023-01-24 16:16:15 +01:00
ndeet
1a54b0cd4a
Merge pull request #7 from ndeet/oc3-modal-checkout
OC3 modal checkout
2023-01-24 16:09:40 +01:00
Andreas Tasch
0288b502c1 Modal checkout; make sure no new order on browser back; fix undefined variable notice; reuse existing invoice if possible. 2023-01-24 16:05:50 +01:00
Andreas Tasch
0d6e5519ca Fix order status on new order. Bump version. 2022-12-27 23:46:47 +01:00
Andreas Tasch
2a1e6984bd Updating greenfield library; bumping version. 2022-12-27 19:11:26 +01:00
Andreas Tasch
6c3da16ce9 Rebuild autoloader. 2022-12-27 18:55:34 +01:00
131 changed files with 653 additions and 815 deletions

View File

@ -24,7 +24,7 @@ jobs:
with:
type: 'zip'
filename: 'btcpay.ocmod.zip'
exclusions: '*.git* *.github* composer.* README.md'
exclusions: '*.git* *.github* composer.*'
- name: Upload artifact to release page.
uses: ncipollo/release-action@v1
with:

View File

@ -1,19 +0,0 @@
# BTCPay for OpenCart extension
This free extension can be used with [OpenCart](https://www.opencart.com) version 3 or 4. You can also find this extension on the OpenCart [marketplace](https://www.opencart.com/index.php?route=marketplace/extension/info&extension_id=44269).
## Installation
You can find a detailed installation guide on how to setup the extension on our [OpenCart documentation](https://docs.btcpayserver.org/OpenCart/).
## Feedback
If you have issues or feature requests feel free to open an [issue](https://github.com/btcpayserver/opencart/issues) or join us on our [chat](https://chat.btcpayserver.org) or [telegram channel](https://t.me/btcpayserver).
Please let us always know your OpenCart, BTCPay extension, PHP versions. You can find them on the extension configuration page.
## Development
OpenCart 4 development happens on the `master` branch and OpenCart 3 is maintained on the `3.x` branch but is mostly in maintenance mode.

View File

@ -1,280 +0,0 @@
<?php
namespace Opencart\Admin\Controller\Extension\Btcpay\Payment;
use BTCPayServer\Client\Store;
use BTCPayServer\Client\Webhook;
require_once DIR_EXTENSION . 'btcpay/system/library/btcpay/autoload.php';
require_once DIR_EXTENSION . 'btcpay/system/library/btcpay/version.php';
class Btcpay extends \Opencart\System\Engine\Controller {
private $error = [];
public function index(): void {
$this->load->language('extension/btcpay/payment/btcpay');
$this->document->setTitle($this->language->get('heading_title'));
$this->load->model('setting/setting');
$this->load->model('localisation/order_status');
$this->load->model('localisation/geo_zone');
$data['save'] = $this->url->link('extension/btcpay/payment/btcpay|save', 'user_token=' . $this->session->data['user_token']);
$data['back'] = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment');
$data['cancel'] = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment', true);
$data['order_statuses'] = $this->model_localisation_order_status->getOrderStatuses();
$data['geo_zones'] = $this->model_localisation_geo_zone->getGeoZones();
if (isset($this->error['warning'])) {
$data['error_warning'] = $this->error['warning'];
} else {
$data['error_warning'] = '';
}
if (isset($this->session->data['success'])) {
$data['success'] = $this->session->data['success'];
unset($this->session->data['success']);
} else {
$data['success'] = '';
}
$data['breadcrumbs'] = [];
$data['breadcrumbs'][] = array(
'text' => $this->language->get('text_home'),
'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'], true)
);
$data['breadcrumbs'][] = array(
'text' => $this->language->get('text_extension'),
'href' => $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment', true)
);
$data['breadcrumbs'][] = array(
'text' => $this->language->get('heading_title'),
'href' => $this->url->link('extension/btcpay/payment/btcpay', 'user_token=' . $this->session->data['user_token'], true)
);
$fields = [
'payment_btcpay_status',
'payment_btcpay_url',
'payment_btcpay_api_auth_token',
'payment_btcpay_btcpay_storeid',
'payment_btcpay_webhook',
'payment_btcpay_webhook_delete',
'payment_btcpay_modal_mode',
'payment_btcpay_new_status_id',
'payment_btcpay_paid_status_id',
'payment_btcpay_settled_status_id',
'payment_btcpay_settled_paidover_status_id',
'payment_btcpay_invalid_status_id',
'payment_btcpay_expired_status_id',
'payment_btcpay_expired_partialpayment_status_id',
'payment_btcpay_expired_paidlate_status_id',
'payment_btcpay_refunded_status_id',
'payment_btcpay_total',
'payment_btcpay_geo_zone_id',
'payment_btcpay_debug_mode',
];
// Process our fields to be sure they are displayed.
foreach ($fields as $field) {
if (isset($this->request->post[$field])) {
$data[$field] = $this->request->post[$field];
} else {
$data[$field] = $this->config->get($field);
}
}
$data['payment_btcpay_sort_order'] = isset($this->request->post['payment_btcpay_sort_order']) ?
$this->request->post['payment_btcpay_sort_order'] : $this->config->get('payment_btcpay_sort_order');
$data['header'] = $this->load->controller('common/header');
$data['column_left'] = $this->load->controller('common/column_left');
$data['footer'] = $this->load->controller('common/footer');
$this->response->setOutput($this->load->view('extension/btcpay/payment/btcpay', $data));
}
protected function validate($messages): array {
$this->load->language('extension/btcpay/payment/btcpay');
if (!$this->user->hasPermission('modify', 'extension/btcpay/payment/btcpay')) {
$messages['error'] = $this->language->get('error_permission');
}
if (!class_exists('BTCPayServer\Client\Health')) {
$messages['error'] = $this->language->get('error_composer');
}
if (!isset($messages['error'])) {
$host = $this->request->post['payment_btcpay_url'];
$apiKey = $this->request->post['payment_btcpay_api_auth_token'];
$storeId = $this->request->post['payment_btcpay_btcpay_storeid'];
try {
$client = new Store($host, $apiKey);
$store = $client->getStore($storeId);
if (empty($store->getId())) {
$messages['error'] = $this->language->get('error_store_not_found');
}
} catch (\Throwable $e) {
$messages['error'] = $this->language->get('error_connect_to_btcpay');
$this->log->write($e->getMessage());
}
}
return $messages;
}
public function save(): void {
$this->load->language('extension/btcpay/payment/btcpay');
$this->load->model('setting/setting');
$json = [];
$redirect = false;
$json = $this->validate($json);
if (empty($json['error'])) {
$host = $this->request->post['payment_btcpay_url'];
$apiKey = $this->request->post['payment_btcpay_api_auth_token'];
$storeId = $this->request->post['payment_btcpay_btcpay_storeid'];
// On saving we create a webhook if there is none yet.
if ($this->webhookExists() === false) {
if ($whData = $this->webhookSetup($host, $apiKey, $storeId)) {
$this->request->post['payment_btcpay_webhook'] = $whData;
$json['success'] = $this->language->get('notice_success_create_webhook');
$redirect = true;
} else {
$json['error'] = $this->language->get('error_creating_webhook');
}
} else {
// Check if the user wants to delete an existing webhook.
if (isset($this->request->post['payment_btcpay_webhook_delete']) &&
$this->request->post['payment_btcpay_webhook_delete'] === '1'
) {
// Try to delete the webhook on the provided host.
$this->webhookDelete($host, $apiKey, $storeId);
unset($this->request->post['payment_btcpay_webhook']);
unset($this->request->post['payment_btcpay_webhook_delete']);
$json['success'] = $this->language->get('notice_success_delete_webhook');
$redirect = true;
} else {
// Need to convert existing webhook values back to array for storage.
if (isset($this->request->post['payment_btcpay_webhook'])) {
$whString = $this->request->post['payment_btcpay_webhook'];
$whString = str_replace(['ID: ', 'SECRET: ', 'URL: '], '', $whString);
$whArr = explode(' | ', $whString);
if (count($whArr) === 3) {
$whData = [
'id' => $whArr[0],
'secret' => $whArr[1],
'url' => $whArr[2]
];
$this->request->post['payment_btcpay_webhook'] = $whData;
}
}
}
}
}
if (empty($json['error'])) {
if (!empty($json['success'])) {
$json['success'] = $this->language->get('notice_success') . ' ' . $json['success'];
} else {
$json['success'] = $this->language->get('notice_success');
}
$this->model_setting_setting->editSetting('payment_btcpay', $this->request->post);
}
if ($redirect) {
$this->session->data['success'] = $json['success'];
unset($json['success']);
$json['redirect'] = $this->url->link('extension/btcpay/payment/btcpay', 'user_token=' . $this->session->data['user_token'], true);
}
$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($json));
}
public function install(): void {
$this->load->model('extension/btcpay/payment/btcpay');
$this->model_extension_btcpay_payment_btcpay->install();
}
public function uninstall(): void {
$this->load->model('extension/btcpay/payment/btcpay');
$this->model_extension_btcpay_payment_btcpay->uninstall();
}
private function webhookExists(): bool {
// Check if the config is any value set at all.
$data = $this->config->get('payment_btcpay_webhook');
if (empty($data) || !is_array($data)) {
return false;
}
// todo: load webhook form BTCPay to check if the callback url domain is the same
return true;
}
private function webhookSetup($host, $apiKey, $storeId): ?array {
$whEvents = [
'InvoiceReceivedPayment',
'InvoicePaymentSettled',
'InvoiceProcessing',
'InvoiceExpired',
'InvoiceSettled',
'InvoiceInvalid'
];
try {
$whClient = new Webhook( $host, $apiKey );
$webhook = $whClient->createWebhook(
$storeId,
$this->webhookCallbackUrl(),
$whEvents,
null
);
// Prepare data for settings storage.
$whData = [
'id' => $webhook->getData()['id'],
'secret' => $webhook->getData()['secret'],
'url' => $webhook->getData()['url']
];
return $whData;
} catch (\Throwable $e) {
$this->log->write($e->getMessage());
}
return NULL;
}
private function webhookDelete($host, $apiKey, $storeId): void {
$data = $this->config->get('payment_btcpay_webhook');
$client = new \BTCPayServer\Client\Webhook($host, $apiKey);
try {
$client->deleteWebhook($storeId, $data['id']);
} catch (\Throwable $e) {
$this->log->write('Error deleting webhook: ' . $e->getMessage());
}
}
private function webhookCallbackUrl(): string {
$url = $this->url->link('extension/btcpay/payment/btcpay|callback', '', true);
// As we are in admin controller context we need to strip out the admin
// path to receive the correct frontend callback url.
$adminPathParts = explode('/', DIR_APPLICATION);
end($adminPathParts); // Last array item is empty.
$adminPath = prev($adminPathParts);
if (!empty($adminPath)) {
$url = str_replace($adminPath . '/', '', $url);
}
return $url;
}
}

View File

@ -1,301 +0,0 @@
{{ header }}{{ column_left }}
<div id="content">
<div class="page-header">
<div class="container-fluid">
<div class="float-end">
<button type="submit" form="form_payment" data-bs-toggle="tooltip" title="{{ button_save }}" class="btn btn-primary"><i class="fas fa-save"></i></button>
<a href="{{ back }}" data-bs-toggle="tooltip" title="{{ button_back }}" class="btn btn-light"><i class="fas fa-reply"></i></a>
</div>
<h1>{{ heading_title }}</h1>
<ul class="breadcrumb">
{% for breadcrumb in breadcrumbs %}
<li class="breadcrumb-item"><a href="{{ breadcrumb['href'] }}">{{ breadcrumb['text'] }}</a></li>
{% endfor %}
</ul>
</div>
</div>
<div class="container-fluid">
{% if error %}
<div class="alert alert-danger alert-dismissible"><i class="fas fa-exclamation-circle"></i> {{ error_warning }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
{% if success %}
<div class="alert alert-success">
{{ success }}
</div>
{% endif %}
<div class="alert alert-info" style="overflow: hidden;">
<div class="row">
<div class="col-sm-12">{{ text_version_info }}</div>
</div>
<div class="row">
<div class="col-sm-12">{{ text_support_info }}</div>
</div>
</div>
<div class="card">
<div class="card-header"><i class="fas fa-pencil-alt"></i> {{ text_edit }}</div>
<div class="card-body">
<form action="{{ save }}" method="post" id="form_payment" data-oc-toggle="ajax">
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-status">{{ entry_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_status" id="input-status" class="form-select">
{% if payment_btcpay_status %}
<option value="1" selected="selected">{{ text_enabled }}</option>
<option value="0">{{ text_disabled }}</option>
{% else %}
<option value="1">{{ text_enabled }}</option>
<option value="0" selected="selected">{{ text_disabled }}</option>
{% endif %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-btcpay-url">{{ entry_btcpay_url }}</label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_url" value="{{ payment_btcpay_url }}" placeholder="{{ entry_btcpay_url }}" id="input-btcpay-url" class="form-control" />
<div class="help-block mt-1">
{{ help_btcpay_url }}
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-api-auth-token">{{ entry_api_auth_token }}</label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_api_auth_token" value="{{ payment_btcpay_api_auth_token }}" placeholder="{{ entry_api_auth_token }}" id="input-api-auth-token" class="form-control" />
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-btcpay-storeid">{{ entry_btcpay_storeid }}</label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_btcpay_storeid" value="{{ payment_btcpay_btcpay_storeid }}" placeholder="{{ entry_btcpay_btcpay_storeid }}" id="input-btcpay-storeid" class="form-control" />
</div>
</div>
<div class="row mb-3">
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-webhook">{{ entry_webhook }}</label>
<div class="col-sm-10">
{% if payment_btcpay_webhook.id %}
{% set whData = 'ID: ' ~ payment_btcpay_webhook.id ~ ' | SECRET: ' ~ payment_btcpay_webhook.secret ~ ' | URL: ' ~ payment_btcpay_webhook.url %}
{% else %}
{% set whData = '-- webhook not configured yet --' %}
{% endif %}
<input readonly="readonly" type="text" name="payment_btcpay_webhook" value="{{ whData }}" placeholder="{{ entry_webhook }}" id="input-webhook" class="form-control" />
<div class="help-block">
{{ help_webhook }}
</div>
</div>
</div>
<div class="row">
<label class="col-sm-2 col-form-label" for="input-webhook-delete">{{ entry_webhook_delete }}</label>
<div class="col-sm-10">
<input type="checkbox" name="payment_btcpay_webhook_delete" value="1" id="input-webhook-delete" class="form-check mt-2" {% if payment_btcpay_webhook_delete %} checked="checked" {% endif %} {% if not payment_btcpay_webhook.id %}disabled="disabled"{% endif %}/>
<div class="help-block mt-1">
{{ help_webhook_delete }}
</div>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-modal-mode">{{ entry_modal_mode }}</label>
<div class="col-sm-10">
<input type="checkbox" name="payment_btcpay_modal_mode" value="1" id="input-modal-mode" class="form-check mt-2" {% if payment_btcpay_modal_mode %} checked="checked" {% endif %} />
<div class="help-block mt-1">
{{ help_modal_mode }}
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-new-status">{{ entry_new_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_new_status_id" id="input-new-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_new_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-paid-status">{{ entry_paid_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_paid_status_id" id="input-paid-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_paid_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-settled-status">{{ entry_settled_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_settled_status_id" id="input-settled-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_settled_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-settled-paidover-status">{{ entry_settled_paidover_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_settled_paidover_status_id" id="input-settled-paidover-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_settled_paidover_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-invalid-status">{{ entry_invalid_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_invalid_status_id" id="input-invalid-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_invalid_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-expired-status">{{ entry_expired_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_expired_status_id" id="input-expired-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_expired_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-expired-partialpayment-status">{{ entry_expired_partialpayment_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_expired_partialpayment_status_id" id="input-expired-partialpayment-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_expired_partialpayment_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-expired-paidlate-status">{{ entry_expired_paidlate_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_expired_paidlate_status_id" id="input-expired-paidlate-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_expired_partialpayment_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-refunded-status">{{ entry_refunded_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_refunded_status_id" id="input-refunded-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_refunded_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-total"><span data-toggle="tooltip" title="{{ help_total }}">{{ entry_total }}</span></label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_total" value="{{ payment_btcpay_total }}" placeholder="{{ entry_total }}" id="input-total" class="form-control" />
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-geo-zone">{{ entry_geo_zone }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_geo_zone_id" id="input-geo-zone" class="form-select">
<option value="0">{{ text_all_zones }}</option>
{% for geo_zone in geo_zones %}
{% if geo_zone.geo_zone_id == payment_btcpay_geo_zone_id %}
<option value="{{ geo_zone.geo_zone_id }}" selected="selected">{{ geo_zone.name }}</option>
{% else %}
<option value="{{ geo_zone.geo_zone_id }}">{{ geo_zone.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-sort-order">{{ entry_sort_order }}</label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_sort_order" value="{{ payment_btcpay_sort_order }}" placeholder="{{ entry_sort_order }}" id="input-sort-order" class="form-control" />
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-debug-mode">{{ entry_debug_mode }}</label>
<div class="col-sm-10">
<input type="checkbox" name="payment_btcpay_debug_mode" value="1" id="input-debug-mode" class="form-check" {% if payment_btcpay_debug_mode %} checked="checked" {% endif %} />
<div class="help-block mt-1">
{{ help_debug_mode }}
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{{ footer }}

View File

@ -1,80 +0,0 @@
<?php
namespace Opencart\Catalog\Model\Extension\Btcpay\Payment;
class Btcpay extends \Opencart\System\Engine\Model
{
public function addOrder(array $data): bool
{
return $this->db->query(
"INSERT INTO `" . DB_PREFIX . "btcpay_order` SET `order_id` = '" . (int)$data['order_id'] . "', `token` = '" . $this->db->escape(
$data['token']
) . "', `invoice_id` = '" . $this->db->escape(
$data['invoice_id']
) . "'"
);
}
public function getOrder(int $order_id): array
{
$query = $this->db->query(
"SELECT * FROM `" . DB_PREFIX . "btcpay_order` WHERE `order_id` = '" . $order_id . "' ORDER BY btcpay_order_id DESC LIMIT 1"
);
return $query->row;
}
public function getOrderByInvoiceId(string $invoice_id): array
{
$query = $this->db->query(
"SELECT * FROM `" . DB_PREFIX . "btcpay_order` WHERE `invoice_id` = '" . $invoice_id . "' LIMIT 1"
);
return $query->row;
}
public function getMethods(array $address = []): array
{
$this->load->language('extension/btcpay/payment/btcpay');
$qStr = "SELECT * FROM `" . DB_PREFIX . "zone_to_geo_zone` WHERE `geo_zone_id` = '" . (int)$this->config->get(
'payment_btcpay_geo_zone_id'
) . "'";
if (isset($address['country_id'])) {
$qStr .= " AND `country_id` = '" . (int)$address['country_id'] ."'";
}
if (isset($address['zone_id'])) {
$qStr .= " AND (`zone_id` = '" . (int)$address['zone_id'] . "' OR `zone_id` = '0')";
}
$query = $this->db->query($qStr);
if (!$this->config->get('payment_btcpay_geo_zone_id')) {
$status = true;
} elseif ($query->num_rows) {
$status = true;
} else {
$status = false;
}
$method_data = [];
if ($status) {
$option_data['btcpay'] = [
'code' => 'btcpay.btcpay',
'name' => $this->language->get('text_title')
];
$method_data = [
'code' => 'btcpay',
'name' => $this->language->get('text_title'),
'option' => $option_data,
'sort_order' => $this->config->get('payment_btcpay_sort_order')
];
}
return $method_data;
}
}

View File

@ -1,6 +1,6 @@
{
"name": "btcpayserver/opencart",
"description": "BTCPay Server plugin for OpenCart 4",
"description": "BTCPay Server plugin for OpenCart 3",
"type": "opencart-extension",
"require": {
"btcpayserver/btcpayserver-greenfield-php": "^1.3"
@ -14,6 +14,6 @@
],
"minimum-stability": "stable",
"config": {
"vendor-dir": "system/library/btcpay"
"vendor-dir": "upload/system/library/btcpay"
}
}

View File

@ -1,7 +0,0 @@
{
"name": "BTCPay Server payment gateway",
"version": "4.2.2",
"license": "MIT",
"author": "BTCPay Server",
"link": "https://btcpayserver.org"
}

View File

@ -1,3 +0,0 @@
<?php
define('BTCPAY_OPENCART_EXTENSION_VERSION', '4.2.2');

View File

@ -0,0 +1,226 @@
<?php
/** Credits: Opencart plugin structure and classes inspired by Coingate payment plugin. */
use BTCPayServer\Client\Store;
use BTCPayServer\Client\Webhook;
require_once DIR_SYSTEM . 'library/btcpay/autoload.php';
require_once DIR_SYSTEM . 'library/btcpay/version.php';
class ControllerExtensionPaymentBTCPay extends Controller {
private $error = [];
public function index() {
$this->load->language('extension/payment/btcpay');
$this->document->setTitle($this->language->get('heading_title'));
$this->load->model('setting/setting');
$this->load->model('localisation/order_status');
$this->load->model('localisation/geo_zone');
if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validate()) {
$host = $this->request->post['payment_btcpay_url'];
$apiKey = $this->request->post['payment_btcpay_api_auth_token'];
$storeId = $this->request->post['payment_btcpay_btcpay_storeid'];
// On saving we create a webhook if there is none yet.
if ($this->webhookExists() === false) {
$whData = $this->webhookSetup($host, $apiKey, $storeId);
$this->request->post['payment_btcpay_webhook'] = $whData;
} else {
// Check if the user wants to delete an existing webhook.
if ($this->request->post['payment_btcpay_webhook_delete'] === '1') {
// Try to delete the webhook on the provided host.
$this->webhookDelete($host, $apiKey, $storeId);
$this->request->post['payment_btcpay_webhook'] = NULL;
$this->request->post['payment_btcpay_webhook_delete'] = NULL;
} else {
// Need to convert existing webhook values back to array for storage.
$whString = $this->request->post['payment_btcpay_webhook'];
$whString = str_replace(['ID: ', 'SECRET: ', 'URL: '], '', $whString);
$whArr = explode(' | ', $whString);
$whData = [
'id' => $whArr[0],
'secret' => $whArr[1],
'url' => $whArr[2]
];
$this->request->post['payment_btcpay_webhook'] = $whData;
}
}
$this->model_setting_setting->editSetting('payment_btcpay', $this->request->post);
$this->session->data['success'] = $this->language->get('notice_success');
$this->response->redirect($this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment', true));
}
$data['action'] = $this->url->link('extension/payment/btcpay', 'user_token=' . $this->session->data['user_token'], true);
$data['cancel'] = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment', true);
$data['order_statuses'] = $this->model_localisation_order_status->getOrderStatuses();
$data['geo_zones'] = $this->model_localisation_geo_zone->getGeoZones();
if (isset($this->error['warning'])) {
$data['error_warning'] = $this->error['warning'];
} else {
$data['error_warning'] = '';
}
$data['breadcrumbs'] = [];
$data['breadcrumbs'][] = array(
'text' => $this->language->get('text_home'),
'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'], true)
);
$data['breadcrumbs'][] = array(
'text' => $this->language->get('text_extension'),
'href' => $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment', true)
);
$data['breadcrumbs'][] = array(
'text' => $this->language->get('heading_title'),
'href' => $this->url->link('extension/payment/btcpay', 'user_token=' . $this->session->data['user_token'], true)
);
$fields = [
'payment_btcpay_status',
'payment_btcpay_url',
'payment_btcpay_api_auth_token',
'payment_btcpay_btcpay_storeid',
'payment_btcpay_webhook',
'payment_btcpay_modal_mode',
'payment_btcpay_webhook_delete',
'payment_btcpay_new_status_id',
'payment_btcpay_paid_status_id',
'payment_btcpay_settled_status_id',
'payment_btcpay_settled_paidover_status_id',
'payment_btcpay_invalid_status_id',
'payment_btcpay_expired_status_id',
'payment_btcpay_expired_partialpayment_status_id',
'payment_btcpay_expired_paidlate_status_id',
'payment_btcpay_refunded_status_id',
'payment_btcpay_total',
'payment_btcpay_geo_zone_id',
'payment_btcpay_debug_mode',
];
// Process our fields to be sure they are displayed.
foreach ($fields as $field) {
if (isset($this->request->post[$field])) {
$data[$field] = $this->request->post[$field];
} else {
$data[$field] = $this->config->get($field);
}
}
$data['payment_btcpay_sort_order'] = isset($this->request->post['payment_btcpay_sort_order']) ?
$this->request->post['payment_btcpay_sort_order'] : $this->config->get('payment_btcpay_sort_order');
$data['header'] = $this->load->controller('common/header');
$data['column_left'] = $this->load->controller('common/column_left');
$data['footer'] = $this->load->controller('common/footer');
$this->response->setOutput($this->load->view('extension/payment/btcpay', $data));
}
protected function validate() {
if (!$this->user->hasPermission('modify', 'extension/payment/btcpay')) {
$this->error['warning'] = $this->language->get('error_permission');
}
if (!class_exists('BTCPayServer\Client\Health')) {
$this->error['warning'] = $this->language->get('error_composer');
}
if (!$this->error) {
$host = $this->request->post['payment_btcpay_url'];
$apiKey = $this->request->post['payment_btcpay_api_auth_token'];
$storeId = $this->request->post['payment_btcpay_btcpay_storeid'];
try {
$client = new Store($host, $apiKey);
$store = $client->getStore($storeId);
if (empty($store->getId())) {
$this->error['warning'] = $this->language->get('error_store_not_found');
}
} catch (\Throwable $e) {
$this->error['warning'] = $this->language->get('error_connect_to_btcpay');
}
}
return !$this->error;
}
public function install() {
$this->load->model('extension/payment/btcpay');
$this->model_extension_payment_btcpay->install();
}
public function uninstall() {
$this->load->model('extension/payment/btcpay');
$this->model_extension_payment_btcpay->uninstall();
}
private function webhookExists() {
// Check if the config is any value set at all.
$data = $this->config->get('payment_btcpay_webhook');
if (empty($data)) {
return false;
}
// todo: load webhook form BTCPay to check if the callback url domain is the same
return true;
}
private function webhookSetup($host, $apiKey, $storeId) {
$whEvents = [
'InvoiceReceivedPayment',
'InvoicePaymentSettled',
'InvoiceProcessing',
'InvoiceExpired',
'InvoiceSettled',
'InvoiceInvalid'
];
try {
$whClient = new Webhook( $host, $apiKey );
$webhook = $whClient->createWebhook(
$storeId,
$this->webhookCallbackUrl(),
$whEvents,
null
);
// Prepare data for settings storage.
$whData = [
'id' => $webhook->getData()['id'],
'secret' => $webhook->getData()['secret'],
'url' => $webhook->getData()['url']
];
return $whData;
} catch (\Throwable $e) {
$this->log->write($e->getMessage());
}
return NULL;
}
private function webhookDelete($host, $apiKey, $storeId) {
$data = $this->config->get('payment_btcpay_webhook');
$client = new \BTCPayServer\Client\Webhook($host, $apiKey);
try {
$client->deleteWebhook($storeId, $data['id']);
} catch (\Throwable $e) {
$this->log->write('Error deleting webhook: ' . $e->getMessage());
}
}
private function webhookCallbackUrl() {
$url = $this->url->link('extension/payment/btcpay/callback', '', true);
// As we are in admin controller context we need to strip out the /admin
// path to receive the carrect frontend callback url.
return str_replace('admin/', '', $url);
}
}

View File

@ -1,11 +1,12 @@
<?php
require_once DIR_EXTENSION . '/btcpay/system/library/btcpay/version.php';
require_once DIR_SYSTEM . 'library/btcpay/version.php';
$_['heading_title'] = 'BTCPay Server';
$_['text_edit'] = 'Edit BTCPay Settings';
$_['text_version_info'] = 'Debug info: OpenCart ' . VERSION . ' with BTCPay Extension ' . BTCPAY_OPENCART_EXTENSION_VERSION . ' on PHP ' . phpversion();
$_['text_support_info'] = 'For setup instructions follow our <a href="https://docs.btcpayserver.org/OpenCart" target="_blank" rel="noopener">setup guide</a>. If you run into any problems feel free to <a href="https://github.com/btcpayserver/opencart" target="_blank" rel="noopener">open an issue on Github</a> or come to our <a href="https://chat.btcpayserver.org/" target="_blank" rel="noopener">Mattermost chat</a>.';
$_['text_extension'] = 'Extensions';
$_['entry_status'] = 'Payment Method Enabled';
@ -33,20 +34,17 @@ $_['entry_debug_mode'] = 'Debug mode';
$_['help_btcpay_url'] = 'The public URL of your BTCPay Server instance. e.g. https://demo.mainnet.btcpayserver.org. You need to have a BTCPay Server instance running, see "Requirements" for several options of deployment on our <a href="https://docs.btcpayserver.org/OpenCart" target="_blank" rel="noopener">setup guide</a>.';
$_['help_webhook'] = 'The webhook will get created automatically after you entered BTCPay Server URL, API Key and Store ID. If you see this field filled with data (after you saved the form) all went well.';
$_['help_webhook_delete'] = 'This is useful if you switch hosts or have problems with webhooks. When checked this will delete the webhook on OpenCart (and BTCPay Server if possible). Make sure to delete the webhook on BTCPay Server Store settings too if not done automatically. <strong>ATTENTION:</strong> You need to edit and <strong>save</strong> this settings page again so a new webhook gets created on BTCPay Server.';
$_['help_modal_mode'] = 'If enabled the invoice will be shown in a modal/overlay (iFrame). Default behaviour is that the user will get redirected to BTCPay Server invoice page.';
$_['help_webhook_delete'] = 'This is useful if you switch hosts or have problems with webhooks. When checked this will delete the webhook on OpenCart (and BTCPay Server if possible). Make sure to delete the webhook on BTCPay Server Store settings too if not done automatically. <strong>ATTENTION:</strong> You need to edit and <strong>save</strong> this settings page again so a new webhook gets created on BTCPay Server.';
$_['help_total'] = 'The checkout total the order must reach before this payment method becomes active.';
$_['help_debug_mode'] = 'If enabled debug output will be saved to the error logs found in System -> Maintenance -> Error logs. Should be disabled after debugging.';
$_['notice_success'] = 'BTCPay Server Payment details have been updated.';
$_['notice_success_delete_webhook'] = 'Successfully deleted webhook. Please save again to create a new one, make sure that on BTCPay Server it does not exist twice.';
$_['notice_success_create_webhook'] = 'Successfully created a webhook on the BTCPay instance.';
$_['notice_success'] = 'BTCPay Server Payment details have been successfully updated.';
$_['notice_success_webhook_renew'] = 'BTCPay Server Payment details have been successfully updated and successfully deleted webhook data.';
$_['error_permission'] = 'Warning: You do not have permission to modify BTCPay Server!';
$_['error_composer'] = 'Unable to load btcpayserver-greenfield-php. Please download a compiled vendor folder or run composer.';
$_['error_store_not_found'] = 'Successfully connected to BTCPay Server but no store with that ID found. Make sure you entered the correct store ID on that corresponding BTCPay Server URL.';
$_['error_connect_to_btcpay'] = 'Error connecting to BTCPay Server instance. Make sure you provided the correct URL, API key.';
$_['error_creating_webhook'] = 'Error creating webhook. Make sure you have a correct store and api key combination with the required permissions. Check OpenCart error logs.';
$_['text_btcpay'] = '<a href="https://btcpayserver.org/" target="_blank" rel="noopener"><img src="/extension/btcpay/admin/view/image/payment/btcpay.png" alt="BTCPay Server" title="BTCPay Server" style="border: 1px solid #EEEEEE;" /></a>';
$_['text_btcpay'] = '<a href="https://btcpayserver.org/" target="_blank" rel="noopener"><img src="view/image/payment/btcpay.png" alt="BTCPay Server" title="BTCPay Server" style="border: 1px solid #EEEEEE;" /></a>';

View File

@ -1,7 +1,7 @@
<?php
namespace Opencart\Admin\Model\Extension\Btcpay\Payment;
class Btcpay extends \Opencart\System\Engine\Model {
public function install(): void {
class ModelExtensionPaymentBTCPay extends Model {
public function install() {
$this->db->query("
CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "btcpay_order` (
`btcpay_order_id` INT(11) NOT NULL AUTO_INCREMENT,
@ -30,7 +30,7 @@ class Btcpay extends \Opencart\System\Engine\Model {
$this->model_setting_setting->editSetting('payment_btcpay', $defaults);
}
public function uninstall(): void {
public function uninstall() {
$this->db->query("DROP TABLE IF EXISTS `" . DB_PREFIX . "btcpay_order`;");
}
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,303 @@
{{ header }}{{ column_left }}
<div id="content">
<div class="page-header">
<div class="container-fluid">
<div class="pull-right">
<button type="submit" form="form-payment" data-toggle="tooltip" title="{{ button_save }}" class="btn btn-primary"><i class="fa fa-save"></i></button>
<a href="{{ cancel }}" data-toggle="tooltip" title="{{ button_cancel }}" class="btn btn-default"><i class="fa fa-reply"></i></a>
</div>
<h1>{{ heading_title }}</h1>
<ul class="breadcrumb">
{% for breadcrumb in breadcrumbs %}
<li><a href="{{ breadcrumb.href }}">{{ breadcrumb.text }}</a></li>
{% endfor %}
</ul>
</div>
</div>
<div class="container-fluid">
{% if error_warning %}
<div class="alert alert-danger alert-dismissible"><i class="fa fa-exclamation-circle"></i> {{ error_warning }}
<button type="button" class="close" data-dismiss="alert">&times;</button>
</div>
{% endif %}
<div class="alert alert-info" style="overflow: hidden;">
<div class="row">
<div class="col-sm-12">{{ text_version_info }}</div>
</div>
<div class="row">
<div class="col-sm-12">{{ text_support_info }}</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-pencil"></i> {{ text_edit }}</h3>
</div>
<div class="panel-body">
<div>
</div>
<form action="{{ action }}" method="post" enctype="multipart/form-data" id="form-payment" class="form-horizontal">
<div class="form-group">
<label class="col-sm-2 control-label" for="input-status">{{ entry_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_status" id="input-status" class="form-control">
{% if payment_btcpay_status %}
<option value="1" selected="selected">{{ text_enabled }}</option>
<option value="0">{{ text_disabled }}</option>
{% else %}
<option value="1">{{ text_enabled }}</option>
<option value="0" selected="selected">{{ text_disabled }}</option>
{% endif %}
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-btcpay-url">{{ entry_btcpay_url }}</label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_url" value="{{ payment_btcpay_url }}" placeholder="{{ entry_btcpay_url }}" id="input-btcpay-url" class="form-control" />
<div class="help-block">
{{ help_btcpay_url }}
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-api-auth-token">{{ entry_api_auth_token }}</label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_api_auth_token" value="{{ payment_btcpay_api_auth_token }}" placeholder="{{ entry_api_auth_token }}" id="input-api-auth-token" class="form-control" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-btcpay-storeid">{{ entry_btcpay_storeid }}</label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_btcpay_storeid" value="{{ payment_btcpay_btcpay_storeid }}" placeholder="{{ entry_btcpay_btcpay_storeid }}" id="input-btcpay-storeid" class="form-control" />
</div>
</div>
<div class="form-group">
<div class="form-row">
<label class="col-sm-2 control-label" for="input-webhook">{{ entry_webhook }}</label>
<div class="col-sm-10">
{% if payment_btcpay_webhook.id %}
{% set whData = 'ID: ' ~ payment_btcpay_webhook.id ~ ' | SECRET: ' ~ payment_btcpay_webhook.secret ~ ' | URL: ' ~ payment_btcpay_webhook.url %}
{% else %}
{% set whData = '-- webhook not configured yet --' %}
{% endif %}
<input readonly="readonly" type="text" name="payment_btcpay_webhook" value="{{ whData }}" placeholder="{{ entry_webhook }}" id="input-webhook" class="form-control" />
<div class="help-block">
{{ help_webhook }}
</div>
</div>
</div>
<div class="form-row">
<label class="col-sm-2 control-label" for="input-webhook-delete">{{ entry_webhook_delete }}</label>
<div class="col-sm-10">
<input type="checkbox" name="payment_btcpay_webhook_delete" value="1" id="input-webhook-delete" class="form-control" {% if payment_btcpay_webhook_delete %} checked="checked" {% endif %} {% if not payment_btcpay_webhook.id %}disabled="disabled"{% endif %}/>
<div class="help-block">
{{ help_webhook_delete }}
</div>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-modal-mode">{{ entry_modal_mode }}</label>
<div class="col-sm-10">
<input type="checkbox" name="payment_btcpay_modal_mode" value="1" id="input-modal-mode" class="form-control" {% if payment_btcpay_modal_mode %} checked="checked" {% endif %} />
<div class="help-block">
{{ help_modal_mode }}
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-new-status">{{ entry_new_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_new_status_id" id="input-new-status" class="form-control">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_new_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-paid-status">{{ entry_paid_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_paid_status_id" id="input-paid-status" class="form-control">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_paid_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-settled-status">{{ entry_settled_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_settled_status_id" id="input-settled-status" class="form-control">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_settled_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-settled-paidover-status">{{ entry_settled_paidover_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_settled_paidover_status_id" id="input-settled-paidover-status" class="form-control">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_settled_paidover_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-invalid-status">{{ entry_invalid_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_invalid_status_id" id="input-invalid-status" class="form-control">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_invalid_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-expired-status">{{ entry_expired_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_expired_status_id" id="input-expired-status" class="form-control">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_expired_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-expired-partialpayment-status">{{ entry_expired_partialpayment_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_expired_partialpayment_status_id" id="input-expired-partialpayment-status" class="form-control">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_expired_partialpayment_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-expired-paidlate-status">{{ entry_expired_paidlate_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_expired_paidlate_status_id" id="input-expired-paidlate-status" class="form-control">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_expired_partialpayment_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-refunded-status">{{ entry_refunded_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_refunded_status_id" id="input-refunded-status" class="form-control">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_refunded_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-total"><span data-toggle="tooltip" title="{{ help_total }}">{{ entry_total }}</span></label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_total" value="{{ payment_btcpay_total }}" placeholder="{{ entry_total }}" id="input-total" class="form-control" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-geo-zone">{{ entry_geo_zone }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_geo_zone_id" id="input-geo-zone" class="form-control">
<option value="0">{{ text_all_zones }}</option>
{% for geo_zone in geo_zones %}
{% if geo_zone.geo_zone_id == payment_btcpay_geo_zone_id %}
<option value="{{ geo_zone.geo_zone_id }}" selected="selected">{{ geo_zone.name }}</option>
{% else %}
<option value="{{ geo_zone.geo_zone_id }}">{{ geo_zone.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-sort-order">{{ entry_sort_order }}</label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_sort_order" value="{{ payment_btcpay_sort_order }}" placeholder="{{ entry_sort_order }}" id="input-sort-order" class="form-control" />
</div>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="input-debug-mode">{{ entry_debug_mode }}</label>
<div class="col-sm-10">
<input type="checkbox" name="payment_btcpay_debug_mode" value="1" id="input-debug-mode" class="form-control" {% if payment_btcpay_debug_mode %} checked="checked" {% endif %} />
<div class="help-block">
{{ help_debug_mode }}
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{{ footer }}

View File

@ -1,37 +1,29 @@
<?php
namespace Opencart\Catalog\Controller\Extension\Btcpay\Payment;
use BTCPayServer\Client\Invoice;
use BTCPayServer\Client\InvoiceCheckoutOptions;
use BTCPayServer\Client\Webhook;
use BTCPayServer\Util\PreciseNumber;
require_once DIR_EXTENSION . 'btcpay/system/library/btcpay/autoload.php';
require_once DIR_EXTENSION . 'btcpay/system/library/btcpay/version.php';
require DIR_SYSTEM . 'library/btcpay/autoload.php';
require DIR_SYSTEM . 'library/btcpay/version.php';
class Btcpay extends \Opencart\System\Engine\Controller
class ControllerExtensionPaymentBTCPay extends Controller
{
public function index(): string
public function index()
{
$this->load->language('extension/btcpay/payment/btcpay');
$this->load->language('extension/payment/btcpay');
$this->load->model('checkout/order');
$useModal = $this->config->get('payment_btcpay_modal_mode');
$data['button_confirm'] = $this->language->get('button_confirm');
$data['action'] = $this->url->link(
'extension/btcpay/payment/btcpay|checkout',
'extension/payment/btcpay/checkout',
'',
true
);
if (isset($this->session->data['error_warning'])) {
$data['error_warning'] = $this->session->data['error_warning'];
unset($this->session->data['error_warning']);
} else {
$data['error_warning'] = '';
}
if ($useModal) {
$host = $this->config->get('payment_btcpay_url');
$data['btcpay_host'] = $host;
@ -39,18 +31,17 @@ class Btcpay extends \Opencart\System\Engine\Controller
$data['success_link'] = $this->url->link('checkout/success', '', true);
$data['invoice_expired_text'] = $this->language->get('invoice_expired_text');
return $this->load->view('extension/btcpay/payment/btcpay_modal', $data);
return $this->load->view('extension/payment/btcpay_modal', $data);
} else {
// Redirect.
return $this->load->view('extension/btcpay/payment/btcpay', $data);
return $this->load->view('extension/payment/btcpay', $data);
}
}
public function checkout(): void
public function checkout()
{
$this->load->model('checkout/order');
$this->load->model('extension/btcpay/payment/btcpay');
$this->load->language('extension/btcpay/payment/btcpay');
$this->load->model('extension/payment/btcpay');
$debug = $this->config->get('payment_btcpay_debug_mode');
$useModal = $this->config->get('payment_btcpay_modal_mode');
@ -63,23 +54,13 @@ class Btcpay extends \Opencart\System\Engine\Controller
if (!isset($this->session->data['order_id'])) {
$this->log->write('No session data order_id present, aborting.');
return;
return false;
}
$order_info = $this->model_checkout_order->getOrder(
$this->session->data['order_id']
);
if (empty($order_info)) {
if ($debug) {
$this->log->write('Could not load order passed by session, order id: ' . $this->session->data['order_id']);
}
$this->session->data['error_warning'] = $this->language->get('session_checkout_order_error');
$this->response->redirect(
$this->url->link('checkout/checkout', '', true)
);
}
$invoiceId = '';
$checkoutLink = '';
@ -87,6 +68,7 @@ class Btcpay extends \Opencart\System\Engine\Controller
if ($existingInvoice = $this->orderHasExistingInvoice($order_info)) {
$invoiceId = $existingInvoice->getId();
$checkoutLink = $existingInvoice->getCheckoutLink();
if ($debug) {
$this->log->write('Found existing and not yet expired invoice: ' . $invoiceId);
}
@ -98,30 +80,11 @@ class Btcpay extends \Opencart\System\Engine\Controller
$checkoutLink = $newInvoice->getCheckoutLink();
// Add invoiceId to the btcpay order table.
$this->model_extension_btcpay_payment_btcpay->addOrder([
'order_id' => $order_info['order_id'],
'token' => $token,
'invoice_id' => $invoiceId,
]);
$this->model_checkout_order->addHistory(
$order_info['order_id'],
$this->config->get('payment_btcpay_new_status_id'),
'BTCPay invoice id: ' . $newInvoice->getId()
);
/* TODO: wip have BTCPay Server invoice link in customer comments, needs option.
// Add user facing comment with a link to BTCPay Server invoice:
$this->model_checkout_order->addHistory(
$order_info['order_id'],
$this->config->get('payment_btcpay_new_status_id'),
$this->language->get(
'order_payment_link'
) . '<a href="' . $newInvoice->getCheckoutLink() . '" target="_blank">' . $newInvoice->getCheckoutLink() . '</a>',
true
);
*/
$this->model_extension_payment_btcpay->addOrder([
'order_id' => $order_info['order_id'],
'token' => $token,
'invoice_id' => $invoiceId,
]);
}
}
@ -146,27 +109,25 @@ class Btcpay extends \Opencart\System\Engine\Controller
}
}
public function cancel(): void
public function cancel()
{
$this->response->redirect($this->url->link('checkout/cart', ''));
}
public function success(): void
public function success()
{
$this->load->model('checkout/order');
$this->load->model('extension/btcpay/payment/btcpay');
$this->load->model('extension/payment/btcpay');
$debug = $this->config->get('payment_btcpay_debug_mode');
if ($debug) {
$this->log->write('Entering success callback / redirect page.');
$this->log->write('SESSION data: ');
$this->log->write(print_r($this->session->data, true));
}
if (isset($this->session->data['order_id'])) {
$order = $this->model_extension_btcpay_payment_btcpay->getOrder(
$this->session->data['order_id']
if ($order_id = $this->session->data['order_id']) {
$order = $this->model_extension_payment_btcpay->getOrder(
$order_id
);
// Check if token is present and valid.
@ -175,7 +136,7 @@ class Btcpay extends \Opencart\System\Engine\Controller
$this->request->get['token']
) !== 0) {
if ($debug) {
$this->log->write('Redirect to home page, had no valid token.');
$this->log->write('Redirect to home page, request had no valid token.');
}
$this->response->redirect(
$this->url->link('common/home', '', true)
@ -199,9 +160,7 @@ class Btcpay extends \Opencart\System\Engine\Controller
public function callback()
{
$this->load->model('checkout/order');
$this->load->model('extension/btcpay/payment/btcpay');
$json = [];
$this->load->model('extension/payment/btcpay');
$debug = $this->config->get('payment_btcpay_debug_mode');
@ -230,16 +189,10 @@ class Btcpay extends \Opencart\System\Engine\Controller
die($whMessage);
}
$btcpay_order = $this->model_extension_btcpay_payment_btcpay->getOrderByInvoiceId(
$btcpay_order = $this->model_extension_payment_btcpay->getOrderByInvoiceId(
$data->invoiceId
);
if (empty($btcpay_order)) {
$this->log->write('Order not found. Aborting.');
http_response_code(200);
die();
}
$order_info = $this->model_checkout_order->getOrder(
$btcpay_order['order_id']
);
@ -248,7 +201,7 @@ class Btcpay extends \Opencart\System\Engine\Controller
$this->log->write('Order:');
$this->log->write($btcpay_order);
$this->log->write('Webhook payload: ');
$this->log->write(print_r($data, true));
$this->log->write($data);
}
if (!empty($order_info) && !empty($btcpay_order)) {
@ -270,7 +223,7 @@ class Btcpay extends \Opencart\System\Engine\Controller
if ($debug) {
$this->log->write('Invoice data: ');
$this->log->write(print_r($invoice, true));
$this->log->write($invoice);
}
$invStatus = $invoice->getStatus();
@ -278,7 +231,7 @@ class Btcpay extends \Opencart\System\Engine\Controller
if ($invoice) {
$order_status = NULL;
$order_message = 'Event: ' . $data->type . ': ';
$order_message = 'Event: ' . $data->type;
$notify = false;
switch ($data->type) {
case "InvoiceReceivedPayment":
@ -334,7 +287,7 @@ class Btcpay extends \Opencart\System\Engine\Controller
}
if (!is_null($order_status)) {
$this->model_checkout_order->addHistory(
$this->model_checkout_order->addOrderHistory(
$btcpay_order['order_id'],
$this->config->get($order_status),
'Payment status update: ' . $order_message,
@ -352,7 +305,6 @@ class Btcpay extends \Opencart\System\Engine\Controller
}
$this->response->addHeader('HTTP/1.1 200 OK');
$this->response->setOutput(json_encode($json));
}
/**
@ -370,14 +322,14 @@ class Btcpay extends \Opencart\System\Engine\Controller
$apiKey = $this->config->get('payment_btcpay_api_auth_token');
$apiHost = $this->config->get('payment_btcpay_url');
$apiStoreId = $this->config->get('payment_btcpay_btcpay_storeid');
$debug = $this->config->get('payment_btcpay_debug_mode');
$client = new Invoice($apiHost, $apiKey);
$debug = $this->config->get('payment_btcpay_debug_mode');
// Checkout options.
$checkoutOptions = new InvoiceCheckoutOptions();
$redirectUrl = $this->url->link(
'extension/btcpay/payment/btcpay|success',
'extension/payment/btcpay/success',
['token' => $token],
true
);
@ -429,7 +381,7 @@ class Btcpay extends \Opencart\System\Engine\Controller
// Round to 2 decimals to avoid mismatch.
$totalRounded = round((float) $total->__toString(), 2);
$btcpay_order = $this->model_extension_btcpay_payment_btcpay->getOrder(
$btcpay_order = $this->model_extension_payment_btcpay->getOrder(
$order_info['order_id']
);
@ -464,5 +416,4 @@ class Btcpay extends \Opencart\System\Engine\Controller
);
return PreciseNumber::parseString($total);
}
}

View File

@ -5,5 +5,3 @@ $_['button_confirm'] = 'Pay with Bitcoin';
$_['invoice_expired_text'] = 'The invoice expired. Please try again or choose a different payment method.';
$_['invoice_closed_text'] = 'Payment aborted. Please try again or choose a different payment method.';
$_['invoice_failed_text'] = 'Payment aborted. Error processing the request. Please contact store owner if the problem persists.';
$_['order_payment_link'] = 'You can check your payment status here: ';
$_['session_checkout_order_error'] = 'Your cart order id could not be found, please try to log out and in again.';

View File

@ -0,0 +1,48 @@
<?php
class ModelExtensionPaymentBTCPay extends Model {
public function addOrder($data) {
$this->db->query("INSERT INTO `" . DB_PREFIX . "btcpay_order` SET `order_id` = '" . (int)$data['order_id'] . "', `token` = '" . $this->db->escape($data['token']) . "', `invoice_id` = '" . $this->db->escape($data['invoice_id']) . "'");
}
public function getOrder($order_id) {
$query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "btcpay_order` WHERE `order_id` = '" . (int)$order_id . "' ORDER BY btcpay_order_id DESC LIMIT 1 ");
return $query->row;
}
public function getOrderByInvoiceId($invoice_id) {
$query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "btcpay_order` WHERE `invoice_id` = '" . $invoice_id . "' LIMIT 1");
return $query->row;
}
public function getMethod($address, $total) {
$this->load->language('extension/payment/btcpay');
$query = $this->db->query("SELECT * FROM " . DB_PREFIX . "zone_to_geo_zone WHERE geo_zone_id = '" . (int)$this->config->get('payment_btcpay_geo_zone_id') . "' AND country_id = '" . (int)$address['country_id'] . "' AND (zone_id = '" . (int)$address['zone_id'] . "' OR zone_id = '0')");
if ($this->config->get('payment_btcpay_total') > 0 && $this->config->get('payment_btcpay_total') > $total) {
$status = false;
} elseif (!$this->config->get('payment_btcpay_geo_zone_id')) {
$status = true;
} elseif ($query->num_rows) {
$status = true;
} else {
$status = false;
}
$method_data = [];
if ($status) {
$method_data = array(
'code' => 'btcpay',
'title' => $this->language->get('text_title'),
'terms' => '',
'sort_order' => $this->config->get('payment_btcpay_sort_order')
);
}
return $method_data;
}
}

View File

@ -1,9 +1,6 @@
{% if error_warning %}
<div class="alert alert-danger">{{ error_warning }}</div>
{% endif %}
<form action="{{ action }}" method="post">
<div class="buttons">
<div class="text-end">
<div class="pull-right">
<input type="submit" value="{{ button_confirm }}" class="btn btn-primary" />
</div>
</div>

View File

@ -1,7 +1,7 @@
<script src="{{ modal_url }}"></script>
<div class="buttons">
<div class="text-end">
<div class="pull-right">
<button class="btn btn-primary btcpay-modal">{{ button_confirm }}</button>
</div>
</div>
@ -13,10 +13,10 @@
if (data.invoiceId !== undefined) {
window.btcpay.setApiUrlPrefix('{{ btcpay_host }}');
window.btcpay.showInvoice(data.invoiceId);
} else {
showError('{{ invoice_failed_text }}');
}
let invoice_paid = false;
window.btcpay.onModalReceiveMessage(function (event) {
if (isObject(event.data)) {
console.log('invoiceId: ' + event.data.invoiceId);
@ -43,15 +43,19 @@
}
}
});
const isObject = obj => {
return Object.prototype.toString.call(obj) === '[object Object]'
}
}).fail(function() {
showError('{{ invoice_failed_text }}');
});
const showError = err => {
const errFail = '<div class="alert alert-danger alert-dismissible"><i class="fa-solid fa-circle-exclamation"></i> ' + err + '<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>';
$('#alert').prepend(errFail);
const errFail = '<div class="alert alert-danger alert-dismissible">' + err + '<button type="button" class="close" data-dismiss="alert">×</button></div>';
$(this).closest('.panel-body').first().prepend(errFail);
}
});
</script>

View File

@ -19,24 +19,24 @@ abstract class AbstractResult implements \ArrayAccess
return $this->data;
}
public function offsetExists($offset): bool
public function offsetExists($offset)
{
$data = $this->getData();
return array_key_exists($offset, $data);
}
public function offsetGet($offset): mixed
public function offsetGet($offset)
{
$data = $this->getData();
return $data[$offset] ?? null;
}
public function offsetSet($offset, $value): void
public function offsetSet($offset, $value)
{
throw new \RuntimeException('You should not change the data in a result.');
}
public function offsetUnset($offset): void
public function offsetUnset($offset)
{
throw new \RuntimeException('You should not change the data in a result.');
}

Some files were not shown because too many files have changed in this diff Show More