Merge pull request 'fix(payment_paydunya): fiabilise retour PayDunya et post-traitement paiement' (#3) from fix/payment-paydunya into main
Reviewed-on: #3
This commit is contained in:
@@ -9,42 +9,63 @@ _logger = logging.getLogger(__name__)
|
|||||||
class PaydunyaController(http.Controller):
|
class PaydunyaController(http.Controller):
|
||||||
@http.route('/payment/paydunya/return', type='http', auth='public', methods=['GET'], csrf=False)
|
@http.route('/payment/paydunya/return', type='http', auth='public', methods=['GET'], csrf=False)
|
||||||
def paydunya_return(self, **kwargs):
|
def paydunya_return(self, **kwargs):
|
||||||
"""Handle return from PayDunya after payment attempt."""
|
"""Traite le retour utilisateur PayDunya après tentative de paiement."""
|
||||||
token = kwargs.get('token') or http.request.params.get('token')
|
token = kwargs.get('token') or http.request.params.get('token')
|
||||||
if not token:
|
tx_id = kwargs.get('tx_id') or http.request.params.get('tx_id')
|
||||||
return http.request.redirect('/')
|
sale_order_id = kwargs.get('sale_order_id') or http.request.params.get('sale_order_id')
|
||||||
|
|
||||||
|
tx_model = http.request.env['payment.transaction'].sudo()
|
||||||
|
tx = False
|
||||||
|
if tx_id:
|
||||||
|
try:
|
||||||
|
tx = tx_model.search([('id', '=', int(tx_id))], limit=1)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
tx = False
|
||||||
|
|
||||||
provider = http.request.env['payment.provider'].sudo().search([('code', '=', 'paydunya')], limit=1)
|
provider = http.request.env['payment.provider'].sudo().search([('code', '=', 'paydunya')], limit=1)
|
||||||
if not provider:
|
if not provider and token:
|
||||||
_logger.warning('PayDunya return called but no provider found')
|
_logger.warning('PayDunya return called but no provider found')
|
||||||
return http.request.redirect('/')
|
return http.request.redirect('/')
|
||||||
|
|
||||||
# Build check URL using the confirm endpoint
|
data = {}
|
||||||
base = provider._get_paydunya_api_base()
|
if token and provider:
|
||||||
confirm_url = base + '/checkout-invoice/confirm/{}'.format(token)
|
# Construit l'URL de vérification via l'endpoint de confirmation.
|
||||||
|
base = provider._get_paydunya_api_base()
|
||||||
|
confirm_url = base + '/checkout-invoice/confirm/{}'.format(token)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'PAYDUNYA-MASTER-KEY': provider.paydunya_master_key or '',
|
'PAYDUNYA-MASTER-KEY': provider.paydunya_master_key or '',
|
||||||
'PAYDUNYA-PRIVATE-KEY': provider.paydunya_private_key or '',
|
'PAYDUNYA-PRIVATE-KEY': provider.paydunya_private_key or '',
|
||||||
'PAYDUNYA-TOKEN': provider.paydunya_token or ''
|
'PAYDUNYA-TOKEN': provider.paydunya_token or ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Vérifie le statut de paiement côté PayDunya.
|
||||||
|
resp = requests.get(confirm_url, headers=headers, timeout=15)
|
||||||
|
data = resp.json()
|
||||||
|
_logger.info('PayDunya confirm response: %s', data)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception('Error verifying PayDunya invoice: %s', e)
|
||||||
|
data = {'token': token, 'status': 'failed', 'sale_order_id': sale_order_id, 'tx_id': tx_id}
|
||||||
|
elif tx:
|
||||||
|
# Fallback: si PayDunya ne renvoie pas de token, on s'appuie sur la transaction.
|
||||||
|
data = {'tx_id': tx.id, 'sale_order_id': sale_order_id}
|
||||||
|
else:
|
||||||
|
return http.request.redirect('/')
|
||||||
|
|
||||||
|
# Déclenche la mise à jour de la transaction avec les données confirmées.
|
||||||
try:
|
try:
|
||||||
# GET request to verify status
|
|
||||||
resp = requests.get(confirm_url, headers=headers, timeout=15)
|
|
||||||
data = resp.json()
|
|
||||||
_logger.info('PayDunya confirm response: %s', data)
|
|
||||||
except Exception as e:
|
|
||||||
_logger.exception('Error verifying PayDunya invoice: %s', e)
|
|
||||||
data = {'token': token, 'status': 'failed'}
|
|
||||||
|
|
||||||
# Trigger transaction handling with the confirmed data
|
|
||||||
try:
|
|
||||||
tx_model = http.request.env['payment.transaction'].sudo()
|
|
||||||
handled = tx_model._handle_notification_data(data)
|
handled = tx_model._handle_notification_data(data)
|
||||||
if handled:
|
tx = tx or tx_model._get_tx_from_notification(data)
|
||||||
|
if tx and tx.state == 'done':
|
||||||
|
order = tx._paydunya_get_related_sale_order(sale_order_id=sale_order_id)
|
||||||
|
if order:
|
||||||
|
http.request.session['sale_last_order_id'] = order.id
|
||||||
|
http.request.session['sale_transaction_id'] = tx.id
|
||||||
return http.request.redirect('/shop/confirmation')
|
return http.request.redirect('/shop/confirmation')
|
||||||
|
if handled:
|
||||||
|
return http.request.redirect('/shop/cart')
|
||||||
except Exception:
|
except Exception:
|
||||||
_logger.exception('Error handling PayDunya notification data')
|
_logger.exception('Error handling PayDunya notification data')
|
||||||
|
|
||||||
@@ -52,16 +73,17 @@ class PaydunyaController(http.Controller):
|
|||||||
|
|
||||||
@http.route('/payment/paydunya/cancel', type='http', auth='public', methods=['GET'], csrf=False)
|
@http.route('/payment/paydunya/cancel', type='http', auth='public', methods=['GET'], csrf=False)
|
||||||
def paydunya_cancel(self, **kwargs):
|
def paydunya_cancel(self, **kwargs):
|
||||||
"""Handle cancellation from PayDunya."""
|
"""Traite l'annulation de paiement renvoyée par PayDunya."""
|
||||||
token = kwargs.get('token') or http.request.params.get('token')
|
token = kwargs.get('token') or http.request.params.get('token')
|
||||||
_logger.info('PayDunya payment cancelled: token=%s', token)
|
_logger.info('PayDunya payment cancelled: token=%s', token)
|
||||||
return http.request.redirect('/shop/cart')
|
return http.request.redirect('/shop/cart')
|
||||||
|
|
||||||
@http.route('/payment/paydunya/callback', type='http', auth='public', methods=['GET', 'POST'], csrf=False)
|
@http.route('/payment/paydunya/callback', type='http', auth='public', methods=['GET', 'POST'], csrf=False)
|
||||||
def paydunya_callback(self, **kwargs):
|
def paydunya_callback(self, **kwargs):
|
||||||
"""Handle IPN callback from PayDunya.
|
"""Traite le callback IPN envoyé par PayDunya.
|
||||||
|
|
||||||
PayDunya sends data as application/x-www-form-urlencoded with 'data' key containing JSON.
|
PayDunya peut envoyer des données en `application/x-www-form-urlencoded`
|
||||||
|
dans une clé `data` contenant du JSON.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
payload = {}
|
payload = {}
|
||||||
@@ -84,5 +106,5 @@ class PaydunyaController(http.Controller):
|
|||||||
except Exception:
|
except Exception:
|
||||||
_logger.exception('Error handling PayDunya callback')
|
_logger.exception('Error handling PayDunya callback')
|
||||||
|
|
||||||
# Always return 200 OK to PayDunya
|
# Retourne toujours HTTP 200 à PayDunya.
|
||||||
return 'OK'
|
return 'OK'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
import requests
|
import requests
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
from odoo import models
|
from odoo import models, fields
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -10,13 +10,182 @@ _logger = logging.getLogger(__name__)
|
|||||||
class PaymentTransaction(models.Model):
|
class PaymentTransaction(models.Model):
|
||||||
_inherit = 'payment.transaction'
|
_inherit = 'payment.transaction'
|
||||||
|
|
||||||
|
def _paydunya_assign_transaction_payment_method_line(self):
|
||||||
|
"""Assigne un journal et une méthode de paiement valides à la transaction."""
|
||||||
|
self.ensure_one()
|
||||||
|
if 'payment_method_line_id' not in self._fields:
|
||||||
|
return False
|
||||||
|
if self.payment_method_line_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
journal = False
|
||||||
|
if 'journal_id' in self._fields and self.journal_id:
|
||||||
|
journal = self.journal_id
|
||||||
|
if not journal:
|
||||||
|
journal = self.env['account.journal'].search([
|
||||||
|
('company_id', '=', self.company_id.id),
|
||||||
|
('type', 'in', ['bank', 'cash']),
|
||||||
|
('inbound_payment_method_line_ids', '!=', False),
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
method_line = journal.inbound_payment_method_line_ids[:1] if journal else False
|
||||||
|
if not method_line:
|
||||||
|
return False
|
||||||
|
|
||||||
|
vals = {'payment_method_line_id': method_line.id}
|
||||||
|
if 'journal_id' in self._fields and journal:
|
||||||
|
vals['journal_id'] = journal.id
|
||||||
|
self.write(vals)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _paydunya_extract_sale_order_id(self, payload):
|
||||||
|
"""Extrait l'ID de commande depuis la charge utile retour/callback."""
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
for key in ('sale_order_id', 'order_id'):
|
||||||
|
value = payload.get(key)
|
||||||
|
if value:
|
||||||
|
candidates.append(value)
|
||||||
|
|
||||||
|
custom_data = payload.get('custom_data')
|
||||||
|
if isinstance(custom_data, dict):
|
||||||
|
for key in ('odoo_sale_order_id', 'sale_order_id', 'order_id'):
|
||||||
|
value = custom_data.get(key)
|
||||||
|
if value:
|
||||||
|
candidates.append(value)
|
||||||
|
|
||||||
|
for value in candidates:
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _paydunya_extract_tx_id(self, payload):
|
||||||
|
"""Extrait l'ID de transaction Odoo depuis la charge utile retour/callback."""
|
||||||
|
if not isinstance(payload, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
for key in ('tx_id', 'transaction_id'):
|
||||||
|
value = payload.get(key)
|
||||||
|
if value:
|
||||||
|
candidates.append(value)
|
||||||
|
|
||||||
|
custom_data = payload.get('custom_data')
|
||||||
|
if isinstance(custom_data, dict):
|
||||||
|
value = custom_data.get('odoo_tx_id')
|
||||||
|
if value:
|
||||||
|
candidates.append(value)
|
||||||
|
|
||||||
|
for value in candidates:
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _paydunya_get_related_sale_order(self, sale_order_id=None):
|
||||||
|
"""Retrouve la commande liée à la transaction."""
|
||||||
|
self.ensure_one()
|
||||||
|
if 'sale.order' not in self.env:
|
||||||
|
return False
|
||||||
|
|
||||||
|
sale_order = self.env['sale.order']
|
||||||
|
if sale_order_id:
|
||||||
|
sale_order = self.env['sale.order'].sudo().browse(int(sale_order_id))
|
||||||
|
if sale_order.exists():
|
||||||
|
return sale_order
|
||||||
|
|
||||||
|
if hasattr(self, 'sale_order_ids') and self.sale_order_ids:
|
||||||
|
return self.sale_order_ids[0].sudo()
|
||||||
|
|
||||||
|
ref = (self.reference or '').strip()
|
||||||
|
if ref:
|
||||||
|
possible_names = [ref]
|
||||||
|
for separator in ('-', '_'):
|
||||||
|
if separator in ref:
|
||||||
|
possible_names.append(ref.split(separator, 1)[0])
|
||||||
|
sale_order = self.env['sale.order'].sudo().search([('name', 'in', list(set(possible_names)))], limit=1)
|
||||||
|
if sale_order:
|
||||||
|
return sale_order
|
||||||
|
return self.env['sale.order']
|
||||||
|
|
||||||
|
def _paydunya_finalize_sale_order(self, sale_order_id=None):
|
||||||
|
"""Confirme la commande et crée/poste la facture quand le paiement est validé."""
|
||||||
|
self.ensure_one()
|
||||||
|
if 'sale.order' not in self.env:
|
||||||
|
return False
|
||||||
|
sale_order = self._paydunya_get_related_sale_order(sale_order_id=sale_order_id)
|
||||||
|
if not sale_order:
|
||||||
|
_logger.info('PayDunya: no sale order linked to transaction %s', self.reference)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if sale_order.state in ('draft', 'sent'):
|
||||||
|
sale_order.action_confirm()
|
||||||
|
|
||||||
|
invoices = sale_order.invoice_ids.filtered(lambda inv: inv.state != 'cancel')
|
||||||
|
if not invoices:
|
||||||
|
invoices = sale_order._create_invoices()
|
||||||
|
|
||||||
|
draft_invoices = invoices.filtered(lambda inv: inv.state == 'draft')
|
||||||
|
if draft_invoices:
|
||||||
|
draft_invoices.action_post()
|
||||||
|
|
||||||
|
posted_invoices = invoices.filtered(lambda inv: inv.state == 'posted' and inv.amount_residual > 0)
|
||||||
|
if posted_invoices and 'account.payment.register' in self.env:
|
||||||
|
try:
|
||||||
|
register_ctx = {
|
||||||
|
'active_model': 'account.move',
|
||||||
|
'active_ids': posted_invoices.ids,
|
||||||
|
}
|
||||||
|
wizard = self.env['account.payment.register'].with_context(**register_ctx).create({
|
||||||
|
'payment_date': fields.Date.context_today(self),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Force un journal + une méthode valides pour éviter
|
||||||
|
# l'erreur "Please define a payment method line on your payment."
|
||||||
|
if not wizard.journal_id:
|
||||||
|
company = posted_invoices[:1].company_id
|
||||||
|
journal = self.env['account.journal'].search([
|
||||||
|
('company_id', '=', company.id),
|
||||||
|
('type', 'in', ['bank', 'cash']),
|
||||||
|
('inbound_payment_method_line_ids', '!=', False),
|
||||||
|
], limit=1)
|
||||||
|
if journal:
|
||||||
|
wizard.journal_id = journal
|
||||||
|
|
||||||
|
if not wizard.payment_method_line_id:
|
||||||
|
method_line = (
|
||||||
|
wizard.available_payment_method_line_ids[:1]
|
||||||
|
or wizard.journal_id.inbound_payment_method_line_ids[:1]
|
||||||
|
)
|
||||||
|
if method_line:
|
||||||
|
wizard.payment_method_line_id = method_line.id
|
||||||
|
|
||||||
|
wizard._create_payments()
|
||||||
|
except Exception:
|
||||||
|
_logger.exception(
|
||||||
|
'PayDunya: failed to auto-register invoice payment for transaction %s',
|
||||||
|
self.reference,
|
||||||
|
)
|
||||||
|
|
||||||
|
_logger.info(
|
||||||
|
'PayDunya: finalized sale order %s for transaction %s',
|
||||||
|
sale_order.name,
|
||||||
|
self.reference,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
def _normalize_paydunya_notification_data(self, data):
|
def _normalize_paydunya_notification_data(self, data):
|
||||||
"""Normalize PayDunya notification payload across callback and confirm formats."""
|
"""Normalise les payloads PayDunya (callback/IPN et confirmation)."""
|
||||||
payload = data
|
payload = data
|
||||||
if not isinstance(payload, dict):
|
if not isinstance(payload, dict):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Callback can send {"data": "<json>"} or {"data": {...}}
|
# Le callback peut envoyer {"data": "<json>"} ou {"data": {...}}.
|
||||||
if 'data' in payload:
|
if 'data' in payload:
|
||||||
wrapped = payload.get('data')
|
wrapped = payload.get('data')
|
||||||
if isinstance(wrapped, str):
|
if isinstance(wrapped, str):
|
||||||
@@ -27,14 +196,14 @@ class PaymentTransaction(models.Model):
|
|||||||
elif isinstance(wrapped, dict):
|
elif isinstance(wrapped, dict):
|
||||||
payload = wrapped
|
payload = wrapped
|
||||||
|
|
||||||
# Confirm endpoint can send {"response_data": {...}}
|
# L'endpoint de confirmation peut envoyer {"response_data": {...}}.
|
||||||
if isinstance(payload, dict) and isinstance(payload.get('response_data'), dict):
|
if isinstance(payload, dict) and isinstance(payload.get('response_data'), dict):
|
||||||
payload = payload['response_data']
|
payload = payload['response_data']
|
||||||
|
|
||||||
return payload if isinstance(payload, dict) else {}
|
return payload if isinstance(payload, dict) else {}
|
||||||
|
|
||||||
def _get_paydunya_channel(self, processing_values):
|
def _get_paydunya_channel(self, processing_values):
|
||||||
"""Return the PayDunya channel to force based on selected payment method."""
|
"""Détermine le canal PayDunya à forcer selon le moyen de paiement choisi."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
||||||
selected_code = (
|
selected_code = (
|
||||||
@@ -43,7 +212,7 @@ class PaymentTransaction(models.Model):
|
|||||||
or getattr(getattr(self, 'payment_method_id', False), 'code', False)
|
or getattr(getattr(self, 'payment_method_id', False), 'code', False)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Odoo checkout often sends only payment_method_id; resolve it to code.
|
# Le checkout Odoo envoie souvent seulement `payment_method_id`.
|
||||||
if not selected_code:
|
if not selected_code:
|
||||||
pm_id = processing_values.get('payment_method_id')
|
pm_id = processing_values.get('payment_method_id')
|
||||||
if pm_id:
|
if pm_id:
|
||||||
@@ -68,10 +237,10 @@ class PaymentTransaction(models.Model):
|
|||||||
return channel_map.get(str(selected_code).lower())
|
return channel_map.get(str(selected_code).lower())
|
||||||
|
|
||||||
def _get_specific_rendering_values(self, processing_values):
|
def _get_specific_rendering_values(self, processing_values):
|
||||||
"""Create invoice on PayDunya and return rendering values for redirection."""
|
"""Crée la facture côté PayDunya et renvoie les données de redirection."""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
provider = False
|
provider = False
|
||||||
# try common fields used across Odoo versions
|
# Compatibilité entre versions Odoo pour récupérer le provider.
|
||||||
provider = getattr(self, 'provider_id', False) or getattr(self, 'acquirer_id', False)
|
provider = getattr(self, 'provider_id', False) or getattr(self, 'acquirer_id', False)
|
||||||
if not provider:
|
if not provider:
|
||||||
provider = self.env['payment.provider'].search([('code', '=', 'paydunya')], limit=1)
|
provider = self.env['payment.provider'].search([('code', '=', 'paydunya')], limit=1)
|
||||||
@@ -80,14 +249,19 @@ class PaymentTransaction(models.Model):
|
|||||||
|
|
||||||
base = provider._get_paydunya_api_base()
|
base = provider._get_paydunya_api_base()
|
||||||
create_url = base + '/checkout-invoice/create'
|
create_url = base + '/checkout-invoice/create'
|
||||||
|
sale_order = self._paydunya_get_related_sale_order()
|
||||||
|
sale_order_id = sale_order.id if sale_order else False
|
||||||
|
|
||||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||||
return_url = base_url + '/payment/paydunya/return'
|
query = '?tx_id={}'.format(self.id)
|
||||||
cancel_url = base_url + '/payment/paydunya/cancel'
|
if sale_order_id:
|
||||||
callback_url = base_url + '/payment/paydunya/callback'
|
query += '&sale_order_id={}'.format(sale_order_id)
|
||||||
|
return_url = base_url + '/payment/paydunya/return' + query
|
||||||
|
cancel_url = base_url + '/payment/paydunya/cancel' + query
|
||||||
|
callback_url = base_url + '/payment/paydunya/callback' + query
|
||||||
|
|
||||||
# Build payload according to PayDunya documentation
|
# Construit le payload conformément à la documentation PayDunya.
|
||||||
# Required: invoice.total_amount, store.name
|
# Champs requis: invoice.total_amount, store.name.
|
||||||
payload = {
|
payload = {
|
||||||
'invoice': {
|
'invoice': {
|
||||||
'total_amount': int(self.amount),
|
'total_amount': int(self.amount),
|
||||||
@@ -100,10 +274,15 @@ class PaymentTransaction(models.Model):
|
|||||||
'return_url': return_url,
|
'return_url': return_url,
|
||||||
'cancel_url': cancel_url,
|
'cancel_url': cancel_url,
|
||||||
'callback_url': callback_url,
|
'callback_url': callback_url,
|
||||||
}
|
},
|
||||||
|
'custom_data': {
|
||||||
|
'odoo_tx_id': str(self.id),
|
||||||
|
'odoo_sale_order_id': str(sale_order_id or ''),
|
||||||
|
'odoo_reference': self.reference or '',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add customer info if available
|
# Ajoute les informations client si disponibles.
|
||||||
if self.partner_id:
|
if self.partner_id:
|
||||||
payload['invoice']['customer'] = {
|
payload['invoice']['customer'] = {
|
||||||
'name': self.partner_id.name or '',
|
'name': self.partner_id.name or '',
|
||||||
@@ -113,7 +292,7 @@ class PaymentTransaction(models.Model):
|
|||||||
|
|
||||||
channel = self._get_paydunya_channel(processing_values or {})
|
channel = self._get_paydunya_channel(processing_values or {})
|
||||||
if channel:
|
if channel:
|
||||||
# Force a single payment channel so PayDunya redirects to the selected operator flow.
|
# Force un seul canal pour rediriger vers l'opérateur sélectionné.
|
||||||
payload['invoice']['channels'] = [channel]
|
payload['invoice']['channels'] = [channel]
|
||||||
_logger.info(
|
_logger.info(
|
||||||
'PayDunya checkout channel: selected=%s resolved=%s',
|
'PayDunya checkout channel: selected=%s resolved=%s',
|
||||||
@@ -123,7 +302,7 @@ class PaymentTransaction(models.Model):
|
|||||||
channel,
|
channel,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Authentication headers
|
# En-têtes d'authentification API PayDunya.
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'PAYDUNYA-MASTER-KEY': provider.paydunya_master_key or '',
|
'PAYDUNYA-MASTER-KEY': provider.paydunya_master_key or '',
|
||||||
@@ -135,22 +314,22 @@ class PaymentTransaction(models.Model):
|
|||||||
resp = requests.post(create_url, json=payload, headers=headers, timeout=15)
|
resp = requests.post(create_url, json=payload, headers=headers, timeout=15)
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
|
|
||||||
# Check response_code (success = '00')
|
# Vérifie le code de réponse (succès = '00').
|
||||||
response_code = data.get('response_code')
|
response_code = data.get('response_code')
|
||||||
if response_code != '00':
|
if response_code != '00':
|
||||||
_logger.warning('PayDunya API error: %s - %s', response_code, data.get('response_text'))
|
_logger.warning('PayDunya API error: %s - %s', response_code, data.get('response_text'))
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Extract token from response
|
# Extrait le token de la réponse.
|
||||||
token = data.get('token')
|
token = data.get('token')
|
||||||
# response_text is the checkout URL
|
# response_text contient l'URL de checkout.
|
||||||
redirect_url = data.get('response_text')
|
redirect_url = data.get('response_text')
|
||||||
|
|
||||||
if token:
|
if token:
|
||||||
# store reference to match notifications
|
# Stocke la référence provider pour faire le matching des notifications.
|
||||||
self.provider_reference = token
|
self.provider_reference = token
|
||||||
_logger.info('PayDunya invoice created: token=%s, url=%s', token, redirect_url)
|
_logger.info('PayDunya invoice created: token=%s, url=%s', token, redirect_url)
|
||||||
# Return the rendering values expected by the redirect form template.
|
# Renvoie les valeurs attendues par le template de redirection.
|
||||||
return {
|
return {
|
||||||
'api_url': redirect_url,
|
'api_url': redirect_url,
|
||||||
'token': token,
|
'token': token,
|
||||||
@@ -163,27 +342,37 @@ class PaymentTransaction(models.Model):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _get_tx_from_notification(self, data):
|
def _get_tx_from_notification(self, data):
|
||||||
"""Find the transaction corresponding to a PayDunya notification payload."""
|
"""Retrouve la transaction correspondant à une notification PayDunya."""
|
||||||
invoice_data = self._normalize_paydunya_notification_data(data)
|
invoice_data = self._normalize_paydunya_notification_data(data)
|
||||||
|
tx_id = self._paydunya_extract_tx_id(invoice_data)
|
||||||
|
if not tx_id and isinstance(data, dict):
|
||||||
|
tx_id = self._paydunya_extract_tx_id(data)
|
||||||
|
if tx_id:
|
||||||
|
tx = self.search([('id', '=', tx_id)], limit=1)
|
||||||
|
if tx:
|
||||||
|
return tx
|
||||||
|
|
||||||
token = invoice_data.get('invoice', {}).get('token') or invoice_data.get('token')
|
token = invoice_data.get('invoice', {}).get('token') or invoice_data.get('token')
|
||||||
|
if not token and isinstance(data, dict):
|
||||||
|
token = data.get('token') or data.get('invoice_token')
|
||||||
if not token:
|
if not token:
|
||||||
return None
|
return None
|
||||||
tx = self.search([('provider_reference', '=', token)], limit=1)
|
tx = self.search([('provider_reference', '=', token)], limit=1)
|
||||||
return tx or None
|
return tx or None
|
||||||
|
|
||||||
def _handle_notification_data(self, data):
|
def _handle_notification_data(self, data):
|
||||||
"""Handle PayDunya notification payload and update transaction state.
|
"""Traite une notification PayDunya et met à jour l'état de transaction.
|
||||||
|
|
||||||
Validates the hash according to PayDunya documentation (SHA-512 of MASTER-KEY).
|
Valide le hash selon la documentation PayDunya (SHA-512 du MASTER-KEY).
|
||||||
PayDunya sends data as application/x-www-form-urlencoded with key 'data' containing JSON.
|
PayDunya peut envoyer des données `application/x-www-form-urlencoded`
|
||||||
|
avec une clé `data` contenant du JSON.
|
||||||
"""
|
"""
|
||||||
invoice_data = self._normalize_paydunya_notification_data(data)
|
invoice_data = self._normalize_paydunya_notification_data(data)
|
||||||
if not invoice_data:
|
if not invoice_data:
|
||||||
_logger.warning('PayDunya: invalid notification payload: %s', data)
|
_logger.warning('PayDunya: invalid notification payload: %s', data)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Verify hash to ensure the callback is from PayDunya
|
# Vérifie le hash pour s'assurer que le callback vient bien de PayDunya.
|
||||||
provided_hash = invoice_data.get('hash')
|
provided_hash = invoice_data.get('hash')
|
||||||
if not provided_hash and isinstance(data, dict):
|
if not provided_hash and isinstance(data, dict):
|
||||||
provided_hash = data.get('hash')
|
provided_hash = data.get('hash')
|
||||||
@@ -198,12 +387,12 @@ class PaymentTransaction(models.Model):
|
|||||||
expected_hash, provided_hash)
|
expected_hash, provided_hash)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
tx = self._get_tx_from_notification(invoice_data)
|
tx = self._get_tx_from_notification(data)
|
||||||
if not tx:
|
if not tx:
|
||||||
_logger.warning('PayDunya: no transaction found for notification: %s', invoice_data)
|
_logger.warning('PayDunya: no transaction found for notification: %s', invoice_data)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Extract status (valid values: pending, completed, cancelled, failed)
|
# Extrait le statut (pending, completed, cancelled, failed).
|
||||||
status = invoice_data.get('status') or (
|
status = invoice_data.get('status') or (
|
||||||
invoice_data.get('invoice', {}).get('status')
|
invoice_data.get('invoice', {}).get('status')
|
||||||
)
|
)
|
||||||
@@ -214,22 +403,49 @@ class PaymentTransaction(models.Model):
|
|||||||
|
|
||||||
status = str(status).lower()
|
status = str(status).lower()
|
||||||
if status == 'completed':
|
if status == 'completed':
|
||||||
if hasattr(tx, '_set_transaction_done'):
|
sale_order_id = self._paydunya_extract_sale_order_id(invoice_data)
|
||||||
|
tx._paydunya_assign_transaction_payment_method_line()
|
||||||
|
if hasattr(tx, '_set_done'):
|
||||||
|
tx._set_done()
|
||||||
|
elif hasattr(tx, '_set_transaction_done'):
|
||||||
tx._set_transaction_done()
|
tx._set_transaction_done()
|
||||||
else:
|
else:
|
||||||
tx.state = 'done'
|
tx.state = 'done'
|
||||||
|
if hasattr(tx, '_post_process'):
|
||||||
|
try:
|
||||||
|
tx._post_process()
|
||||||
|
except Exception:
|
||||||
|
_logger.exception(
|
||||||
|
'PayDunya: _post_process failed for transaction %s; keeping done state',
|
||||||
|
tx.reference,
|
||||||
|
)
|
||||||
|
if 'is_post_processed' in tx._fields:
|
||||||
|
tx.is_post_processed = True
|
||||||
|
tx._paydunya_finalize_sale_order(sale_order_id=sale_order_id)
|
||||||
_logger.info('PayDunya: Transaction %s marked as done', tx.reference)
|
_logger.info('PayDunya: Transaction %s marked as done', tx.reference)
|
||||||
return True
|
return True
|
||||||
if status in ('cancelled', 'failed'):
|
if status in ('cancelled', 'failed'):
|
||||||
if hasattr(tx, '_set_transaction_cancel'):
|
if hasattr(tx, '_set_canceled'):
|
||||||
|
tx._set_canceled()
|
||||||
|
elif hasattr(tx, '_set_transaction_cancel'):
|
||||||
tx._set_transaction_cancel()
|
tx._set_transaction_cancel()
|
||||||
else:
|
else:
|
||||||
tx.state = 'cancel'
|
tx.state = 'cancel'
|
||||||
|
if hasattr(tx, '_post_process'):
|
||||||
|
try:
|
||||||
|
tx._post_process()
|
||||||
|
except Exception:
|
||||||
|
_logger.exception(
|
||||||
|
'PayDunya: _post_process failed for cancelled/failed transaction %s',
|
||||||
|
tx.reference,
|
||||||
|
)
|
||||||
_logger.info('PayDunya: Transaction %s marked as cancelled/failed', tx.reference)
|
_logger.info('PayDunya: Transaction %s marked as cancelled/failed', tx.reference)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# fallback: mark as pending
|
# Fallback: marque la transaction en attente.
|
||||||
if hasattr(tx, '_set_transaction_pending'):
|
if hasattr(tx, '_set_pending'):
|
||||||
|
tx._set_pending()
|
||||||
|
elif hasattr(tx, '_set_transaction_pending'):
|
||||||
tx._set_transaction_pending()
|
tx._set_transaction_pending()
|
||||||
else:
|
else:
|
||||||
tx.state = 'pending'
|
tx.state = 'pending'
|
||||||
|
|||||||
Reference in New Issue
Block a user