# 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 {}
