Merge pull request #19 from SatoDri/19.0-fix-security-and-ipn
Fix amount tampering, broken IPN URL and invoice status handling (19.0)
This commit is contained in:
6 files changed
+310
-128
No files matched your search
@@ -25,7 +25,7 @@
|
||||
'author': 'BTCPay Server team and contributors',
|
||||
'website': 'https://github.com/btcpayserver/odoo',
|
||||
'category': 'Accounting/Payment Providers',
|
||||
'version': '19.0.1.0',
|
||||
'version': '19.0.1.1',
|
||||
'license': 'GPL-3',
|
||||
'currency': 'USD',
|
||||
'application': False,
|
||||
@@ -39,8 +39,10 @@
|
||||
'data/payment_provider_data.xml',
|
||||
],
|
||||
'images': ['static/description/BTCPay-Odoo-17-featured.png'],
|
||||
# The BTCPay client (legacy BitPay API) is vendored in models/libs and
|
||||
# only requires 'ecdsa' (plus 'requests', shipped with Odoo).
|
||||
'external_dependencies': {
|
||||
'python': ['btcpay-python']
|
||||
'python': ['ecdsa']
|
||||
},
|
||||
'post_init_hook': 'post_init_hook',
|
||||
'uninstall_hook': 'uninstall_hook',
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
# ******************************************************************************
|
||||
|
||||
import json
|
||||
import pprint
|
||||
|
||||
import werkzeug
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import _, http
|
||||
@@ -41,85 +39,270 @@ class BTCPayController(http.Controller):
|
||||
_notify_url = '/payment/btcpay/ipn'
|
||||
_return_url = '/payment/btcpay/return'
|
||||
|
||||
@http.route(_checkout_url, type='http', auth='public', csrf=False, website=True)
|
||||
def checkout(self, **data):
|
||||
@staticmethod
|
||||
def _btcpay_client(provider):
|
||||
return BTCPayClient(
|
||||
host=provider.btcpay_location,
|
||||
pem=provider.btcpay_privateKey,
|
||||
tokens={provider.btcpay_facade: provider.btcpay_token},
|
||||
)
|
||||
|
||||
_logger.info("CHECKOUT: received data:\n%s", pprint.pformat(data))
|
||||
|
||||
# Look up the transaction by reference
|
||||
reference = data.get('reference')
|
||||
tx_sudo = request.env['payment.transaction'].sudo().search([
|
||||
@staticmethod
|
||||
def _find_transaction(reference):
|
||||
return request.env['payment.transaction'].sudo().search([
|
||||
('reference', '=', reference),
|
||||
('provider_code', '=', 'btcpayserver'),
|
||||
], limit=1)
|
||||
|
||||
@staticmethod
|
||||
def _lock_transaction(tx_sudo):
|
||||
"""Lock a transaction until the current database transaction ends.
|
||||
|
||||
Checkout and IPN requests can arrive concurrently. Serializing them on
|
||||
the payment transaction prevents two checkout requests from creating
|
||||
separate invoices and makes an IPN re-check the invoice ID after a
|
||||
checkout replaces an expired invoice.
|
||||
"""
|
||||
tx_sudo.ensure_one()
|
||||
tx_sudo.flush_recordset()
|
||||
tx_sudo.env.cr.execute(
|
||||
"SELECT id FROM payment_transaction WHERE id = %s FOR UPDATE",
|
||||
[tx_sudo.id],
|
||||
)
|
||||
tx_sudo.invalidate_recordset([
|
||||
'state', 'btcpay_invoiceId', 'provider_reference',
|
||||
])
|
||||
|
||||
@staticmethod
|
||||
def _ensure_invoice_id_is_unique(tx_sudo, invoice_id):
|
||||
"""Reject an invoice ID already linked to another transaction."""
|
||||
other_tx = tx_sudo.env['payment.transaction'].sudo().search([
|
||||
('id', '!=', tx_sudo.id),
|
||||
('provider_code', '=', 'btcpayserver'),
|
||||
'|',
|
||||
('btcpay_invoiceId', '=', invoice_id),
|
||||
('provider_reference', '=', invoice_id),
|
||||
], limit=1)
|
||||
if other_tx:
|
||||
raise ValidationError(_(
|
||||
"BTCPay: The invoice is already linked to another transaction."
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def _validate_invoice_binding(
|
||||
cls, tx_sudo, incoming_invoice_id, incoming_order_id, fetched_invoice):
|
||||
"""Bind the untrusted IPN payload to one authenticated invoice and transaction."""
|
||||
if not isinstance(fetched_invoice, dict):
|
||||
raise ValidationError(_("BTCPay: Received an invalid invoice response."))
|
||||
|
||||
fetched_invoice_id = fetched_invoice.get('id')
|
||||
fetched_order_id = fetched_invoice.get('orderId')
|
||||
if not (
|
||||
incoming_invoice_id
|
||||
and incoming_invoice_id == fetched_invoice_id
|
||||
and incoming_invoice_id == tx_sudo.btcpay_invoiceId
|
||||
and incoming_invoice_id == tx_sudo.provider_reference
|
||||
):
|
||||
raise ValidationError(_(
|
||||
"BTCPay: The notification invoice does not match the transaction."
|
||||
))
|
||||
if not (
|
||||
incoming_order_id
|
||||
and incoming_order_id == fetched_order_id
|
||||
and incoming_order_id == tx_sudo.reference
|
||||
):
|
||||
raise ValidationError(_(
|
||||
"BTCPay: The notification order does not match the transaction."
|
||||
))
|
||||
cls._ensure_invoice_id_is_unique(tx_sudo, incoming_invoice_id)
|
||||
|
||||
@classmethod
|
||||
def _find_transaction_by_invoice_id(cls, invoice_id):
|
||||
"""Find exactly one BTCPay transaction from its stored invoice ID."""
|
||||
if not isinstance(invoice_id, str) or not invoice_id:
|
||||
raise ValidationError(_("BTCPay: Missing invoice ID in notification data."))
|
||||
|
||||
txs_sudo = request.env['payment.transaction'].sudo().search([
|
||||
('provider_code', '=', 'btcpayserver'),
|
||||
('btcpay_invoiceId', '=', invoice_id),
|
||||
], limit=2)
|
||||
if len(txs_sudo) != 1:
|
||||
raise ValidationError(_(
|
||||
"BTCPay: No unique transaction found matching the invoice ID."
|
||||
))
|
||||
return txs_sudo
|
||||
|
||||
@staticmethod
|
||||
def _validate_invoice_amount(tx_sudo, invoice):
|
||||
"""Reject reuse of an invoice with a different amount or currency."""
|
||||
try:
|
||||
amount = float(invoice.get('price'))
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
amount = None
|
||||
if (
|
||||
amount is None
|
||||
or invoice.get('currency') != tx_sudo.currency_id.name
|
||||
or tx_sudo.currency_id.compare_amounts(amount, tx_sudo.amount) != 0
|
||||
):
|
||||
raise ValidationError(_(
|
||||
"BTCPay: The existing invoice amount or currency does not match the transaction."
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def _validate_existing_invoice(cls, tx_sudo, invoice_id, invoice):
|
||||
"""Validate a stored invoice before redirecting the customer to it."""
|
||||
cls._validate_invoice_binding(
|
||||
tx_sudo, invoice_id, tx_sudo.reference, invoice,
|
||||
)
|
||||
if not isinstance(invoice.get('status'), str) or not invoice['status']:
|
||||
raise ValidationError(_("BTCPay: The existing invoice response is incomplete."))
|
||||
|
||||
@classmethod
|
||||
def _validate_created_invoice(cls, tx_sudo, invoice):
|
||||
"""Validate a newly created invoice before storing its identity."""
|
||||
if not isinstance(invoice, dict):
|
||||
raise ValidationError(_("BTCPay: Received an invalid invoice response."))
|
||||
invoice_id = invoice.get('id')
|
||||
if (
|
||||
not isinstance(invoice_id, str)
|
||||
or not invoice_id
|
||||
or not isinstance(invoice.get('url'), str)
|
||||
or not invoice['url']
|
||||
or invoice.get('orderId') not in (None, tx_sudo.reference)
|
||||
):
|
||||
raise ValidationError(_("BTCPay: The created invoice response is incomplete."))
|
||||
cls._ensure_invoice_id_is_unique(tx_sudo, invoice_id)
|
||||
|
||||
@http.route(_checkout_url, type='http', auth='public', methods=['POST'],
|
||||
csrf=False, website=True)
|
||||
def checkout(self, **data):
|
||||
""" Create the BTCPay invoice and redirect the buyer to it.
|
||||
|
||||
The redirect form only carries the transaction reference. The invoice
|
||||
price, currency and buyer details are read from the transaction
|
||||
server-side and are never taken from the (client-controlled) request
|
||||
data, so a buyer cannot have an invoice created for a tampered amount.
|
||||
"""
|
||||
_logger.info("BTCPay: checkout request for transaction %s", data.get('reference'))
|
||||
reference = data.get('reference')
|
||||
tx_sudo = self._find_transaction(reference)
|
||||
if not tx_sudo:
|
||||
raise ValidationError(
|
||||
_("BTCPay: No transaction found matching reference %s.", reference)
|
||||
)
|
||||
_("BTCPay: No transaction found matching reference %s.", reference))
|
||||
self._lock_transaction(tx_sudo)
|
||||
if tx_sudo.state != 'draft':
|
||||
# Already processed (e.g. double submission): fall back to the
|
||||
# generic payment status page.
|
||||
return request.redirect('/payment/status')
|
||||
|
||||
provider = tx_sudo.provider_id
|
||||
notification_url = str(data.get('notify_url')).replace("http://", "https://")
|
||||
base_url = request.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
redirect_url = urls.url_join(base_url, self._return_url)
|
||||
client = BTCPayClient(host=provider.btcpay_location, pem=provider.btcpay_privateKey, tokens={provider.btcpay_facade: provider.btcpay_token})
|
||||
invoice = client.create_invoice(
|
||||
{"price": data.get('amount'),
|
||||
"currency": data.get('currency_id'),
|
||||
"orderId": data.get('reference'),
|
||||
"token": provider.btcpay_token,
|
||||
"redirectURL": redirect_url,
|
||||
"notificationURL": notification_url,
|
||||
"extendedNotifications": True,
|
||||
"buyer": {"email": data.get('email') or 'noemailavailable@example.com',
|
||||
"name": data.get('name'),
|
||||
"address1": data.get('street'),
|
||||
"locality": data.get('city'),
|
||||
"postalCode": data.get('zip'),
|
||||
"country": data.get('country'),
|
||||
"notify": False}})
|
||||
_logger.info('Invoice %s \n NOTIFY URL: %s', invoice, notification_url)
|
||||
return werkzeug.utils.redirect(invoice['url'])
|
||||
base_url = provider.get_base_url()
|
||||
client = self._btcpay_client(provider)
|
||||
|
||||
@http.route(_notify_url, type='jsonrpc', auth='public', csrf=False)
|
||||
# A browser retry must not create a second payable invoice. Reuse the
|
||||
# stored invoice while it remains payable. If it expired, the new ID is
|
||||
# stored before the lock is released, so late notifications for the old
|
||||
# invoice can no longer select this transaction.
|
||||
if tx_sudo.btcpay_invoiceId:
|
||||
existing_invoice = client.get_invoice(tx_sudo.btcpay_invoiceId)
|
||||
self._validate_existing_invoice(
|
||||
tx_sudo, tx_sudo.btcpay_invoiceId, existing_invoice,
|
||||
)
|
||||
if existing_invoice['status'].lower() != 'expired':
|
||||
self._validate_invoice_amount(tx_sudo, existing_invoice)
|
||||
if (
|
||||
not isinstance(existing_invoice.get('url'), str)
|
||||
or not existing_invoice['url']
|
||||
):
|
||||
raise ValidationError(_(
|
||||
"BTCPay: The existing invoice response is incomplete."
|
||||
))
|
||||
_logger.info(
|
||||
"BTCPay: reusing invoice %s for transaction %s",
|
||||
tx_sudo.btcpay_invoiceId, tx_sudo.reference,
|
||||
)
|
||||
return request.redirect(existing_invoice['url'], local=False)
|
||||
|
||||
invoice = client.create_invoice({
|
||||
"price": tx_sudo.amount,
|
||||
"currency": tx_sudo.currency_id.name,
|
||||
"orderId": tx_sudo.reference,
|
||||
"token": provider.btcpay_token,
|
||||
"redirectURL": urls.url_join(base_url, self._return_url),
|
||||
"notificationURL": urls.url_join(base_url, self._notify_url),
|
||||
"extendedNotifications": True,
|
||||
"buyer": {
|
||||
"email": tx_sudo.partner_email or 'noemailavailable@example.com',
|
||||
"name": tx_sudo.partner_name,
|
||||
"address1": tx_sudo.partner_address,
|
||||
"locality": tx_sudo.partner_city,
|
||||
"postalCode": tx_sudo.partner_zip,
|
||||
"country": tx_sudo.partner_country_id.code,
|
||||
"notify": False,
|
||||
},
|
||||
})
|
||||
self._validate_created_invoice(tx_sudo, invoice)
|
||||
tx_sudo.write({
|
||||
'btcpay_invoiceId': invoice['id'],
|
||||
'provider_reference': invoice['id'],
|
||||
})
|
||||
_logger.info("BTCPay: created invoice %s for transaction %s",
|
||||
invoice['id'], tx_sudo.reference)
|
||||
return request.redirect(invoice['url'], local=False)
|
||||
|
||||
@http.route(_notify_url, type='jsonrpc', auth='public', methods=['POST'], csrf=False)
|
||||
def btcpay_ipn(self, **post):
|
||||
""" BTCPay IPN. """
|
||||
_logger.info('BTCPAY IPN RECEIVED...')
|
||||
data = json.loads(request.httprequest.data)
|
||||
_logger.info("%s", pprint.pformat(data))
|
||||
_logger.info('BTCPay: IPN received')
|
||||
try:
|
||||
reference = data['data']['orderId']
|
||||
invoice_id = data['data']['id']
|
||||
data = json.loads(request.httprequest.data)
|
||||
except (json.JSONDecodeError, TypeError, UnicodeDecodeError):
|
||||
_logger.warning("BTCPay: received malformed JSON notification data")
|
||||
return ''
|
||||
|
||||
# Look up the transaction by reference
|
||||
tx_sudo = request.env['payment.transaction'].sudo().search([
|
||||
('reference', '=', reference),
|
||||
('provider_code', '=', 'btcpayserver'),
|
||||
], limit=1)
|
||||
if not tx_sudo:
|
||||
_logger.warning("No transaction found matching reference %s.", reference)
|
||||
return ''
|
||||
try:
|
||||
ipn_data = data.get('data') if isinstance(data, dict) else None
|
||||
if not isinstance(ipn_data, dict):
|
||||
raise ValidationError(_("BTCPay: Invalid notification data."))
|
||||
incoming_invoice_id = ipn_data.get('id')
|
||||
incoming_order_id = ipn_data.get('orderId')
|
||||
|
||||
provider = tx_sudo.provider_id
|
||||
client = BTCPayClient(host=provider.btcpay_location, pem=provider.btcpay_privateKey,
|
||||
tokens={provider.btcpay_facade: provider.btcpay_token})
|
||||
# Never use the untrusted order ID to select a transaction. The
|
||||
# stored invoice ID is the lookup key.
|
||||
tx_sudo = self._find_transaction_by_invoice_id(incoming_invoice_id)
|
||||
client = self._btcpay_client(tx_sudo.provider_id)
|
||||
|
||||
fetched_invoice = client.get_invoice(invoice_id)
|
||||
_logger.info('fetched_invoice = %s', pprint.pformat(fetched_invoice))
|
||||
# Fetching through the authenticated API establishes the invoice's
|
||||
# current state. Lock afterwards and re-check the stored ID so a
|
||||
# concurrent checkout cannot replace it between lookup and use.
|
||||
fetched_invoice = client.get_invoice(incoming_invoice_id)
|
||||
self._lock_transaction(tx_sudo)
|
||||
self._validate_invoice_binding(
|
||||
tx_sudo, incoming_invoice_id, incoming_order_id, fetched_invoice,
|
||||
)
|
||||
_logger.info(
|
||||
"BTCPay: verified invoice %s with status %s for transaction %s",
|
||||
fetched_invoice.get('id'), fetched_invoice.get('status'), tx_sudo.reference,
|
||||
)
|
||||
|
||||
payment_data = {
|
||||
"reference": fetched_invoice['orderId'],
|
||||
"status": fetched_invoice['status'],
|
||||
"invoiceID": fetched_invoice['id'],
|
||||
"txid": fetched_invoice['url'],
|
||||
"reference": tx_sudo.reference,
|
||||
"status": fetched_invoice.get('status'),
|
||||
"invoiceID": incoming_invoice_id,
|
||||
"txid": fetched_invoice.get('url'),
|
||||
# Settled amount and currency, validated against the transaction
|
||||
# by the payment framework before it is set done.
|
||||
"amount": fetched_invoice.get('price'),
|
||||
"currency": fetched_invoice.get('currency'),
|
||||
}
|
||||
|
||||
# Use the new Odoo 19 _process() pipeline
|
||||
tx_sudo._process('btcpayserver', payment_data)
|
||||
except ValidationError:
|
||||
_logger.exception("Unable to handle the notification data; skipping to acknowledge")
|
||||
except ValidationError as error:
|
||||
_logger.warning("BTCPay: rejected notification: %s", error)
|
||||
return ''
|
||||
|
||||
@http.route(_return_url, type='http', auth="public", methods=['GET'], csrf=False, save_session=False)
|
||||
@http.route(_return_url, type='http', auth="public", methods=['GET'],
|
||||
csrf=False, save_session=False)
|
||||
def btcpay_return_from_redirect(self, **data):
|
||||
""" BTCPay return """
|
||||
_logger.info("BTCPay: user returned to shop after payment")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from odoo import _, api, fields, models
|
||||
from odoo import api, fields, models
|
||||
from .libs.client import BTCPayClient
|
||||
from .libs import crypto
|
||||
|
||||
@@ -21,15 +21,9 @@ class PaymentProvider(models.Model):
|
||||
btcpay_privateKey = fields.Text(string='Private Key', help='Private Key for BTCPay Client. Leave empty, will be autogenerated during pairing.')
|
||||
btcpay_facade = fields.Char(string='Facade', help='Token facade type: merchant/pos/payroll. Keep merchant', default='merchant')
|
||||
|
||||
def create(self, values_list):
|
||||
if self.code == 'btcpayserver':
|
||||
values_list['btcpay_privateKey'] = crypto.generate_privkey()
|
||||
|
||||
return super(PaymentProvider, self).create(values_list)
|
||||
|
||||
@api.onchange('btcpay_pairingCode')
|
||||
def _onchange_pairingCode(self):
|
||||
if not self.btcpay_token and self.code == 'btcpayserver' and not self.btcpay_pairingCode == '':
|
||||
if not self.btcpay_token and self.code == 'btcpayserver' and self.btcpay_pairingCode:
|
||||
self.btcpay_privateKey = crypto.generate_privkey()
|
||||
client = BTCPayClient(host=self.btcpay_location, pem=self.btcpay_privateKey)
|
||||
token = client.pair_client(self.btcpay_pairingCode)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import pprint
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo.addons.payment import utils as payment_utils
|
||||
from odoo.addons.payment.logging import get_payment_logger
|
||||
|
||||
|
||||
@@ -15,13 +15,15 @@ class PaymentTransaction(models.Model):
|
||||
btcpay_invoiceId = fields.Char("Invoice Id")
|
||||
btcpay_txid = fields.Char("Transaction Id")
|
||||
btcpay_status = fields.Char("Transaction Status")
|
||||
api_url = '/btcpay/checkout'
|
||||
checkout_url = '/btcpay/checkout'
|
||||
notify_url = 'payment/btcpay/ipn'
|
||||
|
||||
def _get_specific_rendering_values(self, processing_values):
|
||||
""" Override of payment to return BTCPay-specific rendering values.
|
||||
|
||||
The redirect form only needs to carry the transaction reference: the
|
||||
BTCPay invoice is built server-side (see the checkout controller) from
|
||||
the transaction, so no amount, currency or buyer data is sent through
|
||||
the browser.
|
||||
|
||||
Note: self.ensure_one() from `_get_processing_values`
|
||||
|
||||
:param dict processing_values: The generic and specific processing values of the transaction
|
||||
@@ -29,29 +31,12 @@ class PaymentTransaction(models.Model):
|
||||
:rtype: dict
|
||||
"""
|
||||
res = super()._get_specific_rendering_values(processing_values)
|
||||
|
||||
if self.provider_code != 'btcpayserver':
|
||||
return res
|
||||
|
||||
base_url = self.provider_id.get_base_url()
|
||||
partner_first_name, partner_last_name = payment_utils.split_partner_name(self.partner_name)
|
||||
|
||||
return {
|
||||
'address1': self.partner_address,
|
||||
'amount': self.amount,
|
||||
'city': self.partner_city,
|
||||
'country': self.partner_country_id.code,
|
||||
'currency_code': self.currency_id.name,
|
||||
'email': self.partner_email,
|
||||
'first_name': partner_first_name,
|
||||
'item_name': f"{self.company_id.name}: {self.reference}",
|
||||
'item_number': self.reference,
|
||||
'last_name': partner_last_name,
|
||||
'lc': self.partner_lang,
|
||||
'state': self.partner_state_id.name,
|
||||
'zip_code': self.partner_zip,
|
||||
'api_url': self.checkout_url,
|
||||
'notify_url': base_url + self.notify_url,
|
||||
'api_url': '/btcpay/checkout',
|
||||
'reference': self.reference,
|
||||
}
|
||||
|
||||
@api.model
|
||||
@@ -69,19 +54,28 @@ class PaymentTransaction(models.Model):
|
||||
return payment_data.get('reference')
|
||||
|
||||
def _extract_amount_data(self, payment_data):
|
||||
""" Override of payment to skip amount validation for BTCPay.
|
||||
""" Override of payment to return the settled amount and currency.
|
||||
|
||||
BTCPay invoices handle amount validation on the BTCPay server side,
|
||||
so we skip the Odoo-side validation.
|
||||
The amount and currency are read back from BTCPay on notification and
|
||||
returned here so that the generic amount validation compares them
|
||||
against the transaction. This prevents a tampered or mismatched invoice
|
||||
from confirming the transaction (and thus the order).
|
||||
|
||||
:param dict payment_data: The payment data sent by the provider.
|
||||
:return: None to skip validation.
|
||||
:rtype: None
|
||||
:return: The settled amount data, or ``None`` for other providers.
|
||||
:rtype: dict|None
|
||||
"""
|
||||
if self.provider_code != 'btcpayserver':
|
||||
return super()._extract_amount_data(payment_data)
|
||||
|
||||
return None
|
||||
try:
|
||||
amount = float(payment_data.get('amount'))
|
||||
except (TypeError, ValueError):
|
||||
amount = None
|
||||
return {
|
||||
'amount': amount,
|
||||
'currency_code': payment_data.get('currency'),
|
||||
}
|
||||
|
||||
def _apply_updates(self, payment_data):
|
||||
""" Override of payment to process the transaction based on BTCPay data.
|
||||
@@ -96,23 +90,43 @@ class PaymentTransaction(models.Model):
|
||||
|
||||
_logger.info("BTCPay _apply_updates: %s", pprint.pformat(payment_data))
|
||||
|
||||
self.provider_reference = payment_data.get('reference')
|
||||
self.btcpay_txid = payment_data.get('txid')
|
||||
self.btcpay_status = payment_data.get('status')
|
||||
if not payment_data.get('reference'):
|
||||
raise ValidationError(
|
||||
"BTCPay: " + _("Received payment data with missing reference."))
|
||||
|
||||
if self.btcpay_status in ['paid', 'processing']:
|
||||
# Keep both external-reference fields bound to the authenticated
|
||||
# BTCPay invoice. The controller validates this identity before
|
||||
# entering `_process`.
|
||||
if invoice_id := payment_data.get('invoiceID'):
|
||||
self.btcpay_invoiceId = invoice_id
|
||||
self.provider_reference = invoice_id
|
||||
if payment_data.get('txid'):
|
||||
self.btcpay_txid = payment_data['txid']
|
||||
status = payment_data.get('status')
|
||||
self.btcpay_status = status
|
||||
|
||||
if status in ('paid', 'processing'):
|
||||
self._set_pending(state_message=payment_data.get('pending_reason'))
|
||||
elif self.btcpay_status in ['confirmed', 'complete']:
|
||||
elif status in ('confirmed', 'complete'):
|
||||
self._set_done()
|
||||
elif self.btcpay_status in ['new']:
|
||||
self.btcpay_invoiceId = payment_data.get('invoiceID')
|
||||
elif self.btcpay_status in ['cancel', 'cancelled']:
|
||||
self._set_canceled()
|
||||
elif self.btcpay_status in ['invalid']:
|
||||
_logger.info(
|
||||
elif status == 'new':
|
||||
pass # Invoice created on BTCPay, waiting for the buyer to pay.
|
||||
elif status in ('expired', 'cancel', 'cancelled'):
|
||||
self._set_canceled(
|
||||
state_message="BTCPay: " + _("Invoice status: %s.", status))
|
||||
elif status == 'invalid':
|
||||
_logger.warning(
|
||||
"Received data with invalid payment status (%s) for transaction with reference %s",
|
||||
self.btcpay_status, self.reference
|
||||
status, self.reference
|
||||
)
|
||||
self._set_error(
|
||||
"BTCPay: " + _("Received data with invalid payment status: %s", self.btcpay_status)
|
||||
"BTCPay: " + _("Received data with invalid payment status: %s.", status)
|
||||
)
|
||||
else:
|
||||
_logger.warning(
|
||||
"Received data with unknown payment status (%s) for transaction with reference %s",
|
||||
status, self.reference
|
||||
)
|
||||
self._set_error(
|
||||
"BTCPay: " + _("Received data with unknown payment status: %s.", status)
|
||||
)
|
||||
@@ -1 +1 @@
|
||||
btcpay-python
|
||||
ecdsa
|
||||
@@ -1,24 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- The redirect form is auto-submitted by the payment framework. It only
|
||||
carries the transaction reference; the BTCPay invoice is built
|
||||
server-side from the transaction (amount and currency cannot be
|
||||
tampered with from the browser). -->
|
||||
<template id="redirect_form">
|
||||
<form t-att-action="api_url" method="post">
|
||||
<input type="hidden" name="reference" t-att-value="item_number"/>
|
||||
<input type="hidden" name="name" t-att-value="first_name"/>
|
||||
<input type="hidden" name="email" t-att-value="email"/>
|
||||
<input type="hidden" name="street" t-att-value="address1"/>
|
||||
<input type="hidden" name="zip" t-att-value="zip_code"/>
|
||||
<input type="hidden" name="city" t-att-value="city"/>
|
||||
<input type="hidden" name="currency_id" t-att-value="currency_code"/>
|
||||
<input type="hidden" name="country" t-att-value="country"/>
|
||||
<input type="hidden" name="custom" t-att-value="item_name"/>
|
||||
<input type="hidden" name="amount" t-att-value="amount"/>
|
||||
<input type="hidden" name="notify_url" t-att-value="notify_url"/>
|
||||
<button type="submit" width="100px" t-att-class="submit_class">
|
||||
<img t-if="not submit_txt" src="/static/description/icon.png"/>
|
||||
<span t-if="submit_txt"> <t t-esc="submit_txt"/> <span class="fa fa-long-arrow-right"/>
|
||||
</span>
|
||||
</button>
|
||||
<input type="hidden" name="reference" t-att-value="reference"/>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
||||