import logging import requests import hashlib from odoo import models _logger = logging.getLogger(__name__) class PaymentTransaction(models.Model): _inherit = 'payment.transaction' def _get_specific_rendering_values(self, **kwargs): """Create invoice on PayDunya and return rendering values for redirection.""" self.ensure_one() provider = False # try common fields used across Odoo versions provider = getattr(self, 'provider_id', False) or getattr(self, 'acquirer_id', False) if not provider: provider = self.env['payment.provider'].search([('code', '=', 'paydunya')], limit=1) if not provider: return {} base = provider._get_paydunya_api_base() create_url = base + '/checkout-invoice/create' base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') return_url = base_url + '/payment/paydunya/return' cancel_url = base_url + '/payment/paydunya/cancel' callback_url = base_url + '/payment/paydunya/callback' # Build payload according to PayDunya documentation # Required: invoice.total_amount, store.name payload = { 'invoice': { 'total_amount': int(self.amount), 'description': self.reference or self.name or 'Payment', }, 'store': { 'name': self.env.company.name or 'Store', }, 'actions': { 'return_url': return_url, 'cancel_url': cancel_url, 'callback_url': callback_url, } } # Add customer info if available if self.partner_id: payload['invoice']['customer'] = { 'name': self.partner_id.name or '', 'email': self.partner_id.email or '', 'phone': self.partner_id.phone or '', } # Authentication headers headers = { 'Content-Type': 'application/json', 'PAYDUNYA-MASTER-KEY': provider.paydunya_master_key or '', 'PAYDUNYA-PRIVATE-KEY': provider.paydunya_private_key or '', 'PAYDUNYA-TOKEN': provider.paydunya_token or '' } try: resp = requests.post(create_url, json=payload, headers=headers, timeout=15) data = resp.json() # Check response_code (success = '00') response_code = data.get('response_code') if response_code != '00': _logger.warning('PayDunya API error: %s - %s', response_code, data.get('response_text')) return {} # Extract token from response token = data.get('token') # response_text is the checkout URL redirect_url = data.get('response_text') if token: # store reference to match notifications self.acquirer_reference = token self._cr.commit() _logger.info('PayDunya invoice created: token=%s, url=%s', token, redirect_url) # Return the template name and rendering values expected by Odoo return { 'rendering_template': 'payment_paydunya.paydunya_redirect_form', 'rendering_values': { 'paydunya_token': token, 'redirect_url': redirect_url, } } else: _logger.warning('PayDunya: no token in response: %s', data) except Exception as e: _logger.exception('Error creating PayDunya invoice: %s', e) return {} def _get_tx_from_notification(self, data): """Find the transaction corresponding to a PayDunya notification payload.""" # Extract invoice data if wrapped if isinstance(data, dict) and 'data' in data: invoice_data = data['data'] else: invoice_data = data token = invoice_data.get('invoice', {}).get('token') or invoice_data.get('token') if not token: return None tx = self.search([('acquirer_reference', '=', token)], limit=1) return tx or None def _handle_notification_data(self, data): """Handle PayDunya notification payload and update transaction state. Validates the hash according to PayDunya documentation (SHA-512 of MASTER-KEY). PayDunya sends data as application/x-www-form-urlencoded with key 'data' containing JSON. """ # Extract the actual data if wrapped if isinstance(data, dict) and 'data' in data: invoice_data = data['data'] else: invoice_data = data # Verify hash to ensure the callback is from PayDunya provided_hash = invoice_data.get('hash') if provided_hash: provider = self.env['payment.provider'].search([('code', '=', 'paydunya')], limit=1) if provider and provider.paydunya_master_key: expected_hash = hashlib.sha512( provider.paydunya_master_key.encode() ).hexdigest() if provided_hash != expected_hash: _logger.warning('PayDunya: Hash mismatch! Possible security issue. Expected %s, got %s', expected_hash, provided_hash) return False tx = self._get_tx_from_notification(invoice_data) if not tx: _logger.warning('PayDunya: no transaction found for notification: %s', invoice_data) return False # Extract status (valid values: pending, completed, cancelled, failed) status = invoice_data.get('status') or ( invoice_data.get('invoice', {}).get('status') ) if not status: _logger.warning('PayDunya: notification without status: %s', invoice_data) return False status = str(status).lower() if status == 'completed': if hasattr(tx, '_set_transaction_done'): tx._set_transaction_done() else: tx.state = 'done' _logger.info('PayDunya: Transaction %s marked as done', tx.reference) return True if status in ('cancelled', 'failed'): if hasattr(tx, '_set_transaction_cancel'): tx._set_transaction_cancel() else: tx.state = 'cancel' _logger.info('PayDunya: Transaction %s marked as cancelled/failed', tx.reference) return True # fallback: mark as pending if hasattr(tx, '_set_transaction_pending'): tx._set_transaction_pending() else: tx.state = 'pending' _logger.info('PayDunya: Transaction %s marked as pending', tx.reference) return True