8 Commits
Author SHA1 Message Date
ndeet 457455edf0 Backward compatibilty fix. 2026-08-12 22:15:19 +02:00
ndeet c3d32a04ce Merge pull request #19 from SatoDri/19.0-fix-security-and-ipn
Fix amount tampering, broken IPN URL and invoice status handling (19.0)
2026-08-12 22:11:31 +02:00
Sato DRI 2a470afc5d Merge pull request #2 from ndeet/satodri-19-fixes
Make sure changes from 18.0 are also here:
2026-08-12 09:52:27 +02:00
ndeet cab44f0847 Make sure changes from 18.0 are also here:
* Strict invoice and order identity binding.
* IPN lookup by stored invoice ID instead of untrusted order ID.
* Invoice ID stored as provider_reference and checked for reuse.
* Transaction row locking prevents concurrent duplicate checkouts.
* Existing invoices are reused; expired ones are replaced safely.
* Late notifications for replaced invoices are ignored.
* Reduced sensitive webhook logging and restricted IPN to POST.
2026-08-07 20:22:15 +02:00
SatoDri 303bfce067 Build the BTCPay invoice server-side and validate the settled amount
Security: the checkout controller built the BTCPay invoice from the POSTed
redirect-form data (price, currency, orderId), and _extract_amount_data
returned None to opt out of amount validation, so the settled amount was
never checked against the transaction. A buyer could therefore submit an
arbitrary price and confirm an order after paying far less. The invoice is
now built server-side from the transaction, the redirect form only carries
the reference, and _extract_amount_data returns the amount and currency
settled on BTCPay (read back from the invoice in the IPN) so the framework
validates them against the transaction before it is set done.

Other fixes in the notification path:
- Fix the IPN notification URL: it was concatenated without a leading
  slash (base_url + "payment/btcpay/ipn"), producing an unreachable host
  so notifications never arrived. It is now built with urls.url_join.
- Handle the "expired" invoice status (cancel the transaction) instead of
  leaving it pending forever, and set an error on unknown statuses.
2026-07-20 00:06:39 +02:00
SatoDri 023e4060c5 Remove dead payment.provider create() override
The override guarded on self.code, which is empty on the model-level
recordset passed to create(), so the branch never ran. The private key is
generated during pairing (in the onchange) instead, so the override is
dead code; removing it also keeps copying a provider from regenerating
(and thus invalidating) an already paired key.
2026-07-20 00:06:39 +02:00
SatoDri 9f3f702c9f Declare ecdsa as the module Python dependency
The bundled BTCPay client (models/libs) imports ecdsa, while the manifest
and requirements only listed btcpay-python, which is not used (the client
is vendored). Installations that did not already have ecdsa available
failed the external-dependency check. Declare ecdsa instead and bump the
module version.
2026-07-20 00:06:39 +02:00
ndeet 94eab634ca Update Readme. 2026-03-30 16:45:20 +02:00
7 changed files with 325 additions and 139 deletions

No files matched your search

+3 -3
View File
@@ -1,12 +1,12 @@
# BTCPay Server payment gateway for Odoo 16, 17 and 18 # BTCPay Server payment gateway for Odoo 16, 17, 18 and 19
## Accept bitcoin payments on Odoo 16, 17 and 18 ## Accept bitcoin payments on Odoo 16, 17, 18 and 19
This odoo addon allows you to accept bitcoin and Lightning Network (and other cryptocurrency) payments in your Odoo e-commerce store. This odoo addon allows you to accept bitcoin and Lightning Network (and other cryptocurrency) payments in your Odoo e-commerce store.
![BTCPay Server Banner](https://raw.githubusercontent.com/btcpayserver/odoo/17.0/payment_btcpayserver/static/description/BTCPay-Odoo-17-featured.png) ![BTCPay Server Banner](https://raw.githubusercontent.com/btcpayserver/odoo/17.0/payment_btcpayserver/static/description/BTCPay-Odoo-17-featured.png)
## How to install ## How to install
Please find the installation instructions here: Please find the installation instructions here:
- [Odoo 18 documentation](https://github.com/btcpayserver/odoo/blob/18.0/payment_btcpayserver/README.md) - [Odoo 18 + 19 documentation](https://github.com/btcpayserver/odoo/blob/19.0/payment_btcpayserver/README.md)
- [Odoo 17 documentation](https://github.com/btcpayserver/odoo/blob/17.0/payment_btcpayserver/README.md) - [Odoo 17 documentation](https://github.com/btcpayserver/odoo/blob/17.0/payment_btcpayserver/README.md)
- [Odoo 16 documentation](https://github.com/btcpayserver/odoo/blob/16.0/payment_btcpayserver/README.md) - [Odoo 16 documentation](https://github.com/btcpayserver/odoo/blob/16.0/payment_btcpayserver/README.md)
+4 -2
View File
@@ -25,7 +25,7 @@
'author': 'BTCPay Server team and contributors', 'author': 'BTCPay Server team and contributors',
'website': 'https://github.com/btcpayserver/odoo', 'website': 'https://github.com/btcpayserver/odoo',
'category': 'Accounting/Payment Providers', 'category': 'Accounting/Payment Providers',
'version': '19.0.1.0', 'version': '19.0.1.1',
'license': 'GPL-3', 'license': 'GPL-3',
'currency': 'USD', 'currency': 'USD',
'application': False, 'application': False,
@@ -39,8 +39,10 @@
'data/payment_provider_data.xml', 'data/payment_provider_data.xml',
], ],
'images': ['static/description/BTCPay-Odoo-17-featured.png'], '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': { 'external_dependencies': {
'python': ['btcpay-python'] 'python': ['ecdsa']
}, },
'post_init_hook': 'post_init_hook', 'post_init_hook': 'post_init_hook',
'uninstall_hook': 'uninstall_hook', 'uninstall_hook': 'uninstall_hook',
+251 -68
View File
@@ -20,9 +20,7 @@
# ****************************************************************************** # ******************************************************************************
import json import json
import pprint
import werkzeug
from werkzeug import urls from werkzeug import urls
from odoo import _, http from odoo import _, http
@@ -41,85 +39,270 @@ class BTCPayController(http.Controller):
_notify_url = '/payment/btcpay/ipn' _notify_url = '/payment/btcpay/ipn'
_return_url = '/payment/btcpay/return' _return_url = '/payment/btcpay/return'
@http.route(_checkout_url, type='http', auth='public', csrf=False, website=True) @staticmethod
def checkout(self, **data): def _btcpay_client(provider):
return BTCPayClient(
_logger.info("CHECKOUT: received data:\n%s", pprint.pformat(data)) host=provider.btcpay_location,
pem=provider.btcpay_privateKey,
# Look up the transaction by reference tokens={provider.btcpay_facade: provider.btcpay_token},
reference = data.get('reference')
tx_sudo = request.env['payment.transaction'].sudo().search([
('reference', '=', reference),
('provider_code', '=', 'btcpayserver'),
], limit=1)
if not tx_sudo:
raise ValidationError(
_("BTCPay: No transaction found matching reference %s.", reference)
) )
provider = tx_sudo.provider_id @staticmethod
notification_url = str(data.get('notify_url')).replace("http://", "https://") def _find_transaction(reference):
base_url = request.env['ir.config_parameter'].sudo().get_param('web.base.url') return request.env['payment.transaction'].sudo().search([
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'])
@http.route(_notify_url, type='jsonrpc', auth='public', 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))
try:
reference = data['data']['orderId']
invoice_id = data['data']['id']
# Look up the transaction by reference
tx_sudo = request.env['payment.transaction'].sudo().search([
('reference', '=', reference), ('reference', '=', reference),
('provider_code', '=', 'btcpayserver'), ('provider_code', '=', 'btcpayserver'),
], limit=1) ], 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: if not tx_sudo:
_logger.warning("No transaction found matching reference %s.", reference) raise ValidationError(
return '' _("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 provider = tx_sudo.provider_id
client = BTCPayClient(host=provider.btcpay_location, pem=provider.btcpay_privateKey, base_url = provider.get_base_url()
tokens={provider.btcpay_facade: provider.btcpay_token}) client = self._btcpay_client(provider)
fetched_invoice = client.get_invoice(invoice_id) # A browser retry must not create a second payable invoice. Reuse the
_logger.info('fetched_invoice = %s', pprint.pformat(fetched_invoice)) # 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)
payment_data = { invoice = client.create_invoice({
"reference": fetched_invoice['orderId'], "price": tx_sudo.amount,
"status": fetched_invoice['status'], "currency": tx_sudo.currency_id.name,
"invoiceID": fetched_invoice['id'], "orderId": tx_sudo.reference,
"txid": fetched_invoice['url'], "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)
# Use the new Odoo 19 _process() pipeline @http.route(_notify_url, type='jsonrpc', auth='public', methods=['POST'], csrf=False)
tx_sudo._process('btcpayserver', payment_data) def btcpay_ipn(self, **post):
except ValidationError: """ BTCPay IPN. """
_logger.exception("Unable to handle the notification data; skipping to acknowledge") _logger.info('BTCPay: IPN received')
try:
data = json.loads(request.httprequest.data)
except (json.JSONDecodeError, TypeError, UnicodeDecodeError):
_logger.warning("BTCPay: received malformed JSON notification data")
return '' return ''
@http.route(_return_url, type='http', auth="public", methods=['GET'], csrf=False, save_session=False) 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')
# 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)
# 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": 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'),
}
tx_sudo._process('btcpayserver', payment_data)
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)
def btcpay_return_from_redirect(self, **data): def btcpay_return_from_redirect(self, **data):
""" BTCPay return """ """ BTCPay return """
_logger.info("BTCPay: user returned to shop after payment") _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.client import BTCPayClient
from .libs import crypto 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_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') 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') @api.onchange('btcpay_pairingCode')
def _onchange_pairingCode(self): 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() self.btcpay_privateKey = crypto.generate_privkey()
client = BTCPayClient(host=self.btcpay_location, pem=self.btcpay_privateKey) client = BTCPayClient(host=self.btcpay_location, pem=self.btcpay_privateKey)
token = client.pair_client(self.btcpay_pairingCode) token = client.pair_client(self.btcpay_pairingCode)
@@ -1,8 +1,8 @@
import pprint import pprint
from odoo import _, api, fields, models 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 from odoo.addons.payment.logging import get_payment_logger
@@ -15,13 +15,15 @@ class PaymentTransaction(models.Model):
btcpay_invoiceId = fields.Char("Invoice Id") btcpay_invoiceId = fields.Char("Invoice Id")
btcpay_txid = fields.Char("Transaction Id") btcpay_txid = fields.Char("Transaction Id")
btcpay_status = fields.Char("Transaction Status") 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): def _get_specific_rendering_values(self, processing_values):
""" Override of payment to return BTCPay-specific rendering 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` Note: self.ensure_one() from `_get_processing_values`
:param dict processing_values: The generic and specific processing values of the transaction :param dict processing_values: The generic and specific processing values of the transaction
@@ -29,29 +31,16 @@ class PaymentTransaction(models.Model):
:rtype: dict :rtype: dict
""" """
res = super()._get_specific_rendering_values(processing_values) res = super()._get_specific_rendering_values(processing_values)
if self.provider_code != 'btcpayserver': if self.provider_code != 'btcpayserver':
return res 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 { return {
'address1': self.partner_address, 'api_url': '/btcpay/checkout',
'amount': self.amount, 'reference': self.reference,
'city': self.partner_city, # Keep the pre-19.0.1.1 key during rolling deployments. QWeb
'country': self.partner_country_id.code, # templates live in the database, so an instance restarted before
'currency_code': self.currency_id.name, # the module is upgraded can still render the previous template.
'email': self.partner_email,
'first_name': partner_first_name,
'item_name': f"{self.company_id.name}: {self.reference}",
'item_number': 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.model @api.model
@@ -69,19 +58,28 @@ class PaymentTransaction(models.Model):
return payment_data.get('reference') return payment_data.get('reference')
def _extract_amount_data(self, payment_data): 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, The amount and currency are read back from BTCPay on notification and
so we skip the Odoo-side validation. 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. :param dict payment_data: The payment data sent by the provider.
:return: None to skip validation. :return: The settled amount data, or ``None`` for other providers.
:rtype: None :rtype: dict|None
""" """
if self.provider_code != 'btcpayserver': if self.provider_code != 'btcpayserver':
return super()._extract_amount_data(payment_data) 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): def _apply_updates(self, payment_data):
""" Override of payment to process the transaction based on BTCPay data. """ Override of payment to process the transaction based on BTCPay data.
@@ -96,23 +94,43 @@ class PaymentTransaction(models.Model):
_logger.info("BTCPay _apply_updates: %s", pprint.pformat(payment_data)) _logger.info("BTCPay _apply_updates: %s", pprint.pformat(payment_data))
self.provider_reference = payment_data.get('reference') if not payment_data.get('reference'):
self.btcpay_txid = payment_data.get('txid') raise ValidationError(
self.btcpay_status = payment_data.get('status') "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')) 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() self._set_done()
elif self.btcpay_status in ['new']: elif status == 'new':
self.btcpay_invoiceId = payment_data.get('invoiceID') pass # Invoice created on BTCPay, waiting for the buyer to pay.
elif self.btcpay_status in ['cancel', 'cancelled']: elif status in ('expired', 'cancel', 'cancelled'):
self._set_canceled() self._set_canceled(
elif self.btcpay_status in ['invalid']: state_message="BTCPay: " + _("Invoice status: %s.", status))
_logger.info( elif status == 'invalid':
_logger.warning(
"Received data with invalid payment status (%s) for transaction with reference %s", "Received data with invalid payment status (%s) for transaction with reference %s",
self.btcpay_status, self.reference status, self.reference
) )
self._set_error( 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
View File
@@ -1 +1 @@
btcpay-python ecdsa
@@ -1,24 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <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"> <template id="redirect_form">
<form t-att-action="api_url" method="post"> <form t-att-action="api_url" method="post">
<input type="hidden" name="reference" t-att-value="item_number"/> <input type="hidden" name="reference" t-att-value="reference"/>
<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>
</form> </form>
</template> </template>
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.