Files
Odoo-Modules/fusion_poynt/models/payment_transaction.py
gsinghpal 14fe9ab716 feat: hide authorizer for rental orders, auto-set sale type
Rental orders no longer show the "Authorizer Required?" question or
the Authorizer field. The sale type is automatically set to 'Rentals'
when creating or confirming a rental order. Validation logic also
skips authorizer checks for rental sale type.

Made-with: Cursor
2026-02-25 23:33:23 -05:00

841 lines
32 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import json
import logging
from werkzeug.urls import url_encode
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools.urls import urljoin as url_join
from odoo.addons.fusion_poynt import const
from odoo.addons.fusion_poynt import utils as poynt_utils
from odoo.addons.fusion_poynt.controllers.main import PoyntController
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
poynt_order_id = fields.Char(
string="Poynt Order ID",
readonly=True,
copy=False,
)
poynt_transaction_id = fields.Char(
string="Poynt Transaction ID",
readonly=True,
copy=False,
)
poynt_receipt_data = fields.Text(
string="Poynt Receipt Data",
readonly=True,
copy=False,
help="JSON blob with receipt-relevant fields captured at payment time.",
)
poynt_voided = fields.Boolean(
string="Voided on Poynt",
readonly=True,
copy=False,
default=False,
)
poynt_void_date = fields.Datetime(
string="Voided On",
readonly=True,
copy=False,
)
def _get_provider_sudo(self):
return self.provider_id.sudo()
# === BUSINESS METHODS - PAYMENT FLOW === #
def _get_specific_processing_values(self, processing_values):
"""Override of payment to return Poynt-specific processing values.
For direct (online) payments we create a Poynt order upfront and return
identifiers plus the return URL so the frontend JS can complete the flow.
"""
if self.provider_code != 'poynt':
return super()._get_specific_processing_values(processing_values)
if self.operation == 'online_token':
return {}
poynt_data = self._poynt_create_order_and_authorize()
provider = self._get_provider_sudo()
base_url = provider.get_base_url()
return_url = url_join(
base_url,
f'{PoyntController._return_url}?{url_encode({"reference": self.reference})}',
)
return {
'poynt_order_id': poynt_data.get('order_id', ''),
'poynt_transaction_id': poynt_data.get('transaction_id', ''),
'return_url': return_url,
'business_id': provider.poynt_business_id,
'is_test': provider.state == 'test',
}
def _send_payment_request(self):
"""Override of `payment` to send a payment request to Poynt."""
if self.provider_code != 'poynt':
return super()._send_payment_request()
if self.operation in ('online_token', 'offline'):
return self._poynt_process_token_payment()
poynt_data = self._poynt_create_order_and_authorize()
if poynt_data:
payment_data = {
'reference': self.reference,
'poynt_order_id': poynt_data.get('order_id'),
'poynt_transaction_id': poynt_data.get('transaction_id'),
'poynt_status': poynt_data.get('status', 'AUTHORIZED'),
'funding_source': poynt_data.get('funding_source', {}),
}
self._process('poynt', payment_data)
def _poynt_create_order_and_authorize(self):
"""Create a Poynt order and authorize the transaction.
:return: Dict with order_id, transaction_id, status, and funding_source.
:rtype: dict
"""
try:
provider = self._get_provider_sudo()
order_payload = poynt_utils.build_order_payload(
self.reference, self.amount, self.currency_id,
business_id=provider.poynt_business_id,
store_id=provider.poynt_store_id or '',
)
order_result = provider._poynt_make_request(
'POST', 'orders', payload=order_payload,
)
order_id = order_result.get('id', '')
self.poynt_order_id = order_id
action = 'AUTHORIZE' if provider.capture_manually else 'SALE'
txn_payload = poynt_utils.build_transaction_payload(
action=action,
amount=self.amount,
currency=self.currency_id,
order_id=order_id,
reference=self.reference,
business_id=provider.poynt_business_id,
store_id=provider.poynt_store_id or '',
)
txn_result = provider._poynt_make_request(
'POST', 'transactions', payload=txn_payload,
)
transaction_id = txn_result.get('id', '')
self.poynt_transaction_id = transaction_id
self.provider_reference = transaction_id
return {
'order_id': order_id,
'transaction_id': transaction_id,
'status': txn_result.get('status', ''),
'funding_source': txn_result.get('fundingSource', {}),
}
except ValidationError as e:
self._set_error(str(e))
return {}
def _poynt_process_token_payment(self):
"""Process a payment using a stored token (card on file).
Uses the JWT payment token via POST /cards/tokenize/charge when
available. Falls back to the legacy cardId flow for tokens that
were created before the JWT migration.
"""
try:
provider = self._get_provider_sudo()
action = 'AUTHORIZE' if provider.capture_manually else 'SALE'
payment_jwt = self.token_id.poynt_payment_token
if payment_jwt:
txn_result = provider._poynt_charge_token(
payment_jwt=payment_jwt,
amount=self.amount,
currency=self.currency_id,
action=action,
reference=self.reference,
)
else:
funding_source = {
'type': 'CREDIT_DEBIT',
'card': {
'cardId': self.token_id.poynt_card_id,
},
'entryDetails': {
'customerPresenceStatus': 'MOTO',
'entryMode': 'KEYED',
},
}
order_payload = poynt_utils.build_order_payload(
self.reference, self.amount, self.currency_id,
business_id=provider.poynt_business_id,
store_id=provider.poynt_store_id or '',
)
order_result = provider._poynt_make_request(
'POST', 'orders', payload=order_payload,
)
order_id = order_result.get('id', '')
self.poynt_order_id = order_id
txn_payload = poynt_utils.build_transaction_payload(
action=action,
amount=self.amount,
currency=self.currency_id,
order_id=order_id,
reference=self.reference,
funding_source=funding_source,
business_id=provider.poynt_business_id,
store_id=provider.poynt_store_id or '',
)
txn_result = provider._poynt_make_request(
'POST', 'transactions', payload=txn_payload,
)
transaction_id = txn_result.get('id', '')
self.poynt_transaction_id = transaction_id
self.provider_reference = transaction_id
order_id = txn_result.get('orderIdFromTransaction', '') or \
txn_result.get('orderId', '') or \
getattr(self, 'poynt_order_id', '') or ''
if order_id:
self.poynt_order_id = order_id
payment_data = {
'reference': self.reference,
'poynt_order_id': order_id,
'poynt_transaction_id': transaction_id,
'poynt_status': txn_result.get('status', ''),
'funding_source': txn_result.get('fundingSource', {}),
}
self._process('poynt', payment_data)
except ValidationError as e:
self._set_error(str(e))
def _send_refund_request(self):
"""Override of `payment` to send a refund request to Poynt.
For captured/settled transactions (SALE), we look up the
CAPTURE child via HATEOAS links and use that as ``parentId``.
The ``fundingSource`` is required per Poynt docs.
"""
if self.provider_code != 'poynt':
return super()._send_refund_request()
source_tx = self.source_transaction_id
refund_amount = abs(self.amount)
minor_amount = poynt_utils.format_poynt_amount(refund_amount, self.currency_id)
parent_txn_id = source_tx.poynt_transaction_id or source_tx.provider_reference
provider = self._get_provider_sudo()
try:
txn_data = provider._poynt_make_request(
'GET', f'transactions/{parent_txn_id}',
)
for link in txn_data.get('links', []):
if link.get('rel') == 'CAPTURE' and link.get('href'):
parent_txn_id = link['href']
_logger.info(
"Refund: using captureId %s instead of original %s",
parent_txn_id, source_tx.poynt_transaction_id,
)
break
except (ValidationError, Exception):
_logger.debug(
"Could not fetch parent txn %s, using original ID",
parent_txn_id,
)
try:
refund_payload = {
'action': 'REFUND',
'parentId': parent_txn_id,
'fundingSource': {
'type': 'CREDIT_DEBIT',
},
'amounts': {
'transactionAmount': minor_amount,
'orderAmount': minor_amount,
'currency': self.currency_id.name,
},
'context': {
'source': 'WEB',
'sourceApp': 'odoo.fusion_poynt',
},
'notes': f'Refund for {source_tx.reference}',
}
result = provider._poynt_make_request(
'POST', 'transactions', payload=refund_payload,
)
self.provider_reference = result.get('id', '')
self.poynt_transaction_id = result.get('id', '')
payment_data = {
'reference': self.reference,
'poynt_transaction_id': result.get('id'),
'poynt_status': result.get('status', 'REFUNDED'),
}
self._process('poynt', payment_data)
except ValidationError as e:
self._set_error(str(e))
def _send_capture_request(self):
"""Override of `payment` to send a capture request to Poynt."""
if self.provider_code != 'poynt':
return super()._send_capture_request()
source_tx = self.source_transaction_id
minor_amount = poynt_utils.format_poynt_amount(self.amount, self.currency_id)
try:
capture_payload = {
'action': 'CAPTURE',
'parentId': source_tx.provider_reference,
'amounts': {
'transactionAmount': minor_amount,
'orderAmount': minor_amount,
'currency': self.currency_id.name,
},
'context': {
'source': 'WEB',
'sourceApp': 'odoo.fusion_poynt',
},
}
result = self._get_provider_sudo()._poynt_make_request(
'POST', 'transactions', payload=capture_payload,
)
payment_data = {
'reference': self.reference,
'poynt_transaction_id': result.get('id'),
'poynt_status': result.get('status', 'CAPTURED'),
}
self._process('poynt', payment_data)
except ValidationError as e:
self._set_error(str(e))
def _send_void_request(self):
"""Override of `payment` to send a void request to Poynt.
Uses ``POST /transactions/{transactionId}/void`` -- the dedicated
void endpoint.
"""
if self.provider_code != 'poynt':
return super()._send_void_request()
source_tx = self.source_transaction_id
txn_id = source_tx.provider_reference or source_tx.poynt_transaction_id
try:
result = self._get_provider_sudo()._poynt_make_request(
'POST', f'transactions/{txn_id}/void',
)
payment_data = {
'reference': self.reference,
'poynt_transaction_id': result.get('id', txn_id),
'poynt_status': result.get('status', 'VOIDED'),
}
self._process('poynt', payment_data)
except ValidationError as e:
self._set_error(str(e))
# === ACTION METHODS - VOID === #
def action_poynt_void(self):
"""Void a confirmed Poynt transaction (same-day, before settlement).
For SALE transactions Poynt creates an AUTHORIZE + CAPTURE pair.
Voiding the AUTHORIZE after capture is rejected by the processor,
so we first fetch the transaction, look for a CAPTURE child via
the HATEOAS ``links``, and void that instead.
On success the linked ``account.payment`` is cancelled (reversing
invoice reconciliation) and the Odoo transaction is set to
cancelled. No credit note is created because funds were never
settled.
"""
self.ensure_one()
if self.provider_code != 'poynt':
raise ValidationError(_("This action is only available for Poynt transactions."))
if self.state != 'done':
raise ValidationError(_("Only confirmed transactions can be voided."))
txn_id = self.poynt_transaction_id or self.provider_reference
if not txn_id:
raise ValidationError(_("No Poynt transaction ID found."))
existing_refund = self.env['payment.transaction'].sudo().search([
('source_transaction_id', '=', self.id),
('operation', '=', 'refund'),
('state', '=', 'done'),
], limit=1)
if existing_refund:
raise ValidationError(_(
"This transaction has already been refunded "
"(%(ref)s). A voided transaction and a refund would "
"result in a double reversal.",
ref=existing_refund.reference,
))
provider = self.sudo().provider_id
txn_data = provider._poynt_make_request('GET', f'transactions/{txn_id}')
poynt_status = txn_data.get('status', '')
if poynt_status in ('REFUNDED', 'VOIDED') or txn_data.get('voided'):
raise ValidationError(_(
"This transaction has already been %(status)s on Poynt. "
"It cannot be voided again.",
status=poynt_status.lower() if poynt_status else 'voided',
))
void_target_id = txn_id
for link in txn_data.get('links', []):
child_id = link.get('href', '')
child_rel = link.get('rel', '')
if not child_id:
continue
if child_rel == 'CAPTURE':
void_target_id = child_id
try:
child_data = provider._poynt_make_request(
'GET', f'transactions/{child_id}',
)
child_status = child_data.get('status', '')
if child_status == 'REFUNDED' or child_data.get('voided'):
raise ValidationError(_(
"A linked transaction (%(child_id)s) has already "
"been %(status)s. Voiding would cause a double "
"reversal.",
child_id=child_id,
status='refunded' if child_status == 'REFUNDED' else 'voided',
))
except ValidationError:
raise
except Exception:
continue
_logger.info(
"Voiding Poynt transaction: original=%s, target=%s",
txn_id, void_target_id,
)
already_voided = txn_data.get('voided', False)
if already_voided:
_logger.info("Transaction %s is already voided on Poynt, skipping API call.", txn_id)
result = txn_data
else:
result = provider._poynt_make_request(
'POST', f'transactions/{void_target_id}/void',
)
_logger.info(
"Poynt void response: status=%s, voided=%s, id=%s",
result.get('status'), result.get('voided'), result.get('id'),
)
is_voided = result.get('voided', False)
void_status = result.get('status', '')
if not is_voided and void_status not in ('VOIDED', 'REFUNDED'):
if void_status == 'DECLINED':
raise ValidationError(_(
"Void declined by the payment processor. This usually "
"means the batch has already settled (past the daily "
"closeout at 6:00 PM). Settled transactions cannot be "
"voided.\n\n"
"To reverse this payment, create a Credit Note on the "
"invoice and process a refund through the standard "
"Odoo workflow."
))
raise ValidationError(
_("Poynt did not confirm the void. Status: %(status)s",
status=void_status)
)
if self.payment_id:
self.payment_id.sudo().action_cancel()
self.sudo().write({
'state': 'cancel',
'poynt_voided': True,
'poynt_void_date': fields.Datetime.now(),
})
invoice = self.invoice_ids[:1]
if invoice:
invoice.sudo().message_post(
body=_(
"Payment voided: transaction %(ref)s was voided on Poynt "
"(Poynt void ID: %(void_id)s).",
ref=self.reference,
void_id=result.get('id', ''),
),
)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'success',
'message': _("Transaction voided successfully on Poynt."),
'next': {'type': 'ir.actions.client', 'tag': 'soft_reload'},
},
}
# === BUSINESS METHODS - NOTIFICATION PROCESSING === #
@api.model
def _search_by_reference(self, provider_code, payment_data):
"""Override of payment to find the transaction based on Poynt data."""
if provider_code != 'poynt':
return super()._search_by_reference(provider_code, payment_data)
reference = payment_data.get('reference')
if reference:
tx = self.search([
('reference', '=', reference),
('provider_code', '=', 'poynt'),
])
else:
poynt_txn_id = payment_data.get('poynt_transaction_id')
if poynt_txn_id:
tx = self.search([
('poynt_transaction_id', '=', poynt_txn_id),
('provider_code', '=', 'poynt'),
])
else:
_logger.warning("Received Poynt data with no reference or transaction ID")
tx = self
if not tx:
_logger.warning(
"No transaction found matching Poynt reference %s", reference,
)
return tx
def _extract_amount_data(self, payment_data):
"""Override of `payment` to skip amount validation for Poynt.
Terminal payments may include tips or rounding adjustments, so we
return None to opt out of the strict amount comparison.
"""
if self.provider_code != 'poynt':
return super()._extract_amount_data(payment_data)
return None
def _apply_updates(self, payment_data):
"""Override of `payment` to update the transaction based on Poynt data."""
if self.provider_code != 'poynt':
return super()._apply_updates(payment_data)
poynt_txn_id = payment_data.get('poynt_transaction_id')
if poynt_txn_id:
self.provider_reference = poynt_txn_id
self.poynt_transaction_id = poynt_txn_id
poynt_order_id = payment_data.get('poynt_order_id')
if poynt_order_id:
self.poynt_order_id = poynt_order_id
funding_source = payment_data.get('funding_source', {})
if funding_source:
card_details = poynt_utils.extract_card_details(funding_source)
if card_details.get('brand'):
payment_method = self.env['payment.method']._get_from_code(
card_details['brand'],
mapping=const.CARD_BRAND_MAPPING,
)
if payment_method:
self.payment_method_id = payment_method
status = payment_data.get('poynt_status', '')
if not status:
self._set_error(_("Received data with missing transaction status."))
return
odoo_state = poynt_utils.get_poynt_status(status)
if odoo_state == 'authorized':
self._set_authorized()
elif odoo_state == 'done':
self._set_done()
self._post_process()
self._poynt_generate_receipt(payment_data)
elif odoo_state == 'cancel':
self._set_canceled()
elif odoo_state == 'refund':
self._set_done()
self._post_process()
self._poynt_generate_receipt(payment_data)
elif odoo_state == 'error':
error_msg = payment_data.get('error_message', _("Payment was declined by Poynt."))
self._set_error(error_msg)
else:
_logger.warning(
"Received unknown Poynt status (%s) for transaction %s.",
status, self.reference,
)
self._set_error(
_("Received data with unrecognized status: %s.", status)
)
def _create_payment(self, **extra_create_values):
"""Override to route Poynt payments directly to the bank account.
Card payments via Poynt are settled instantly -- there is no separate
bank reconciliation step. We swap the ``outstanding_account_id`` to
the journal's default (bank) account before posting so the payment
transitions to ``paid`` instead of lingering in ``in_process``.
"""
if self.provider_code != 'poynt':
return super()._create_payment(**extra_create_values)
self.ensure_one()
provider = self._get_provider_sudo()
reference = f'{self.reference} - {self.provider_reference or ""}'
payment_method_line = provider.journal_id.inbound_payment_method_line_ids\
.filtered(lambda l: l.payment_provider_id == provider)
payment_values = {
'amount': abs(self.amount),
'payment_type': 'inbound' if self.amount > 0 else 'outbound',
'currency_id': self.currency_id.id,
'partner_id': self.partner_id.commercial_partner_id.id,
'partner_type': 'customer',
'journal_id': provider.journal_id.id,
'company_id': provider.company_id.id,
'payment_method_line_id': payment_method_line.id,
'payment_token_id': self.token_id.id,
'payment_transaction_id': self.id,
'memo': reference,
'write_off_line_vals': [],
'invoice_ids': self.invoice_ids,
**extra_create_values,
}
payment_term_lines = self.invoice_ids.line_ids.filtered(
lambda line: line.display_type == 'payment_term'
)
if payment_term_lines:
payment_values['destination_account_id'] = payment_term_lines[0].account_id.id
payment = self.env['account.payment'].create(payment_values)
bank_account = provider.journal_id.default_account_id
if bank_account and bank_account.account_type == 'asset_cash':
payment.outstanding_account_id = bank_account
payment.action_post()
self.payment_id = payment
if self.operation == self.source_transaction_id.operation:
invoices = self.source_transaction_id.invoice_ids
else:
invoices = self.invoice_ids
invoices = invoices.filtered(lambda inv: inv.state != 'cancel')
if invoices:
invoices.filtered(lambda inv: inv.state == 'draft').action_post()
(payment.move_id.line_ids + invoices.line_ids).filtered(
lambda line: line.account_id == payment.destination_account_id
and not line.reconciled
).reconcile()
return payment
def _extract_token_values(self, payment_data):
"""Override of `payment` to return token data based on Poynt data."""
if self.provider_code != 'poynt':
return super()._extract_token_values(payment_data)
funding_source = payment_data.get('funding_source', {})
card_details = poynt_utils.extract_card_details(funding_source)
if not card_details:
_logger.warning(
"Tokenization requested but no card data in payment response."
)
return {}
return {
'payment_details': card_details.get('last4', ''),
'poynt_card_id': card_details.get('card_id', ''),
}
# === RECEIPT GENERATION === #
def _poynt_generate_receipt(self, payment_data=None):
"""Fetch transaction details from Poynt, store receipt data, generate
a PDF receipt, and attach it to the linked invoice's chatter.
This method is best-effort: failures are logged but never block
the payment flow.
"""
self.ensure_one()
if self.provider_code != 'poynt' or not self.poynt_transaction_id:
return
try:
self._poynt_store_receipt_data(payment_data)
self._poynt_attach_receipt_pdf()
self._poynt_attach_poynt_receipt()
except Exception:
_logger.exception(
"Receipt generation failed for transaction %s", self.reference,
)
def _poynt_store_receipt_data(self, payment_data=None):
"""Fetch the full Poynt transaction and persist receipt-relevant
fields in :attr:`poynt_receipt_data` as a JSON blob."""
txn_data = {}
try:
txn_data = self._get_provider_sudo()._poynt_make_request(
'GET', f'transactions/{self.poynt_transaction_id}',
)
except (ValidationError, Exception):
_logger.debug(
"Could not fetch Poynt txn %s for receipt", self.poynt_transaction_id,
)
funding = payment_data.get('funding_source', {}) if payment_data else {}
if not funding:
funding = txn_data.get('fundingSource', {})
card = funding.get('card', {})
entry = funding.get('entryDetails', {})
amounts = txn_data.get('amounts', {})
processor = txn_data.get('processorResponse', {})
context = txn_data.get('context', {})
currency_name = amounts.get('currency', self.currency_id.name)
decimals = const.CURRENCY_DECIMALS.get(currency_name, 2)
receipt = {
'transaction_id': self.poynt_transaction_id,
'order_id': self.poynt_order_id or '',
'reference': self.reference,
'status': txn_data.get('status', payment_data.get('poynt_status', '') if payment_data else ''),
'created_at': txn_data.get('createdAt', ''),
'card_type': card.get('type', ''),
'card_last4': card.get('numberLast4', ''),
'card_first6': card.get('numberFirst6', ''),
'card_holder': card.get('cardHolderFullName', ''),
'entry_mode': entry.get('entryMode', ''),
'customer_presence': entry.get('customerPresenceStatus', ''),
'transaction_amount': amounts.get('transactionAmount', 0) / (10 ** decimals) if amounts.get('transactionAmount') else float(self.amount),
'tip_amount': amounts.get('tipAmount', 0) / (10 ** decimals) if amounts.get('tipAmount') else 0,
'currency': currency_name,
'approval_code': processor.get('approvalCode', txn_data.get('approvalCode', '')),
'processor': processor.get('processor', ''),
'processor_status': processor.get('status', ''),
'store_id': context.get('storeId', ''),
'device_id': context.get('storeDeviceId', ''),
}
self.poynt_receipt_data = json.dumps(receipt)
def _poynt_attach_receipt_pdf(self):
"""Render the QWeb receipt report and attach the PDF to the invoice."""
invoice = self.invoice_ids[:1]
if not invoice:
return
try:
report = self.env.ref('fusion_poynt.action_report_poynt_receipt')
pdf_content, _report_type = report._render_qweb_pdf(report.report_name, [self.id])
except Exception:
_logger.debug("Could not render Poynt receipt PDF for %s", self.reference)
return
filename = f"Payment_Receipt_{self.reference}.pdf"
attachment = self.env['ir.attachment'].sudo().create({
'name': filename,
'type': 'binary',
'datas': base64.b64encode(pdf_content),
'res_model': 'account.move',
'res_id': invoice.id,
'mimetype': 'application/pdf',
})
invoice.sudo().message_post(
body=_(
"Payment receipt generated for transaction %(ref)s.",
ref=self.reference,
),
attachment_ids=[attachment.id],
)
def _poynt_attach_poynt_receipt(self):
"""Try the Poynt renderReceipt endpoint and attach the result."""
invoice = self.invoice_ids[:1]
if not invoice:
return
receipt_content = self._get_provider_sudo()._poynt_fetch_receipt(
self.poynt_transaction_id,
)
if not receipt_content:
return
filename = f"Poynt_Receipt_{self.reference}.html"
self.env['ir.attachment'].sudo().create({
'name': filename,
'type': 'binary',
'datas': base64.b64encode(receipt_content.encode('utf-8')),
'res_model': 'account.move',
'res_id': invoice.id,
'mimetype': 'text/html',
})
def _get_poynt_receipt_values(self):
"""Parse the stored receipt JSON for use in QWeb templates.
For refund transactions that lack their own receipt data, this
falls back to the source (sale) transaction's receipt data so the
card details and approval codes are still available.
:return: Dict of receipt values or empty dict.
:rtype: dict
"""
self.ensure_one()
data = self.poynt_receipt_data
if not data and self.source_transaction_id:
data = self.source_transaction_id.poynt_receipt_data
if not data:
return {}
try:
return json.loads(data)
except (json.JSONDecodeError, TypeError):
return {}
def _get_source_receipt_values(self):
"""Return receipt values from the original sale transaction.
Used by the refund receipt template to render the original sale
details on a second page.
"""
self.ensure_one()
if self.source_transaction_id and self.source_transaction_id.poynt_receipt_data:
try:
return json.loads(self.source_transaction_id.poynt_receipt_data)
except (json.JSONDecodeError, TypeError):
pass
return {}