# Part of Odoo. See LICENSE file for full copyright and licensing details. 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, ) # === 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() base_url = self.provider_id.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': self.provider_id.poynt_business_id, 'is_test': self.provider_id.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: order_payload = poynt_utils.build_order_payload( self.reference, self.amount, self.currency_id, ) order_result = self.provider_id._poynt_make_request( 'POST', 'orders', payload=order_payload, ) order_id = order_result.get('id', '') self.poynt_order_id = order_id action = 'AUTHORIZE' if self.provider_id.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, ) txn_result = self.provider_id._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). For token-based payments we send a SALE or AUTHORIZE using the stored card ID from the payment token. """ try: action = 'AUTHORIZE' if self.provider_id.capture_manually else 'SALE' funding_source = { 'type': 'CREDIT_DEBIT', 'card': { 'cardId': self.token_id.poynt_card_id, }, } order_payload = poynt_utils.build_order_payload( self.reference, self.amount, self.currency_id, ) order_result = self.provider_id._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, ) txn_result = self.provider_id._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 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.""" 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) try: refund_payload = { 'action': 'REFUND', 'parentId': source_tx.provider_reference, '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 = self.provider_id._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.provider_id._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.""" if self.provider_code != 'poynt': return super()._send_void_request() source_tx = self.source_transaction_id try: void_payload = { 'action': 'VOID', 'parentId': source_tx.provider_reference, 'context': { 'source': 'WEB', 'sourceApp': 'odoo.fusion_poynt', }, } result = self.provider_id._poynt_make_request( 'POST', 'transactions', payload=void_payload, ) payment_data = { 'reference': self.reference, 'poynt_transaction_id': result.get('id'), 'poynt_status': result.get('status', 'VOIDED'), } self._process('poynt', payment_data) except ValidationError as e: self._set_error(str(e)) # === 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 _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() if self.operation == 'refund': self.env.ref('payment.cron_post_process_payment_tx')._trigger() elif odoo_state == 'cancel': self._set_canceled() elif odoo_state == 'refund': self._set_done() self.env.ref('payment.cron_post_process_payment_tx')._trigger() 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 _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', ''), }