dine360-social-commerce/addons/dine360_meta_social/models/social_whatsapp_notification.py

195 lines
8.0 KiB
Python

# -*- coding: utf-8 -*-
import logging
from odoo import models, fields, api, _
_logger = logging.getLogger(__name__)
class SocialWhatsAppNotification(models.Model):
_name = 'social.whatsapp.notification'
_description = 'WhatsApp Order Notification Log'
_order = 'create_date desc, id desc'
sale_order_id = fields.Many2one('sale.order', string='Sale Order', ondelete='cascade', index=True)
picking_id = fields.Many2one('stock.picking', string='Stock Delivery', ondelete='set null')
partner_id = fields.Many2one('res.partner', string='Customer', related='sale_order_id.partner_id', store=True)
phone_number = fields.Char(string='Recipient Phone', required=True)
event_type = fields.Selection([
('order_confirmed', 'Order Confirmed'),
('order_processing', 'Order Processing'),
('order_shipped', 'Order Shipped'),
('order_delivered', 'Order Delivered'),
('customer_enquiry', 'Customer Enquiry'),
], string='Event', required=True, index=True)
template_id = fields.Many2one('social.whatsapp.template', string='Template Mapping')
template_name = fields.Char(string='Meta Template Name')
parameters_sent = fields.Text(string='Parameters Sent')
state = fields.Selection([
('pending', 'Pending Send'),
('sent', 'Sent'),
('delivered', 'Delivered'),
('read', 'Read'),
('failed', 'Failed'),
], string='Status', default='pending', index=True, required=True)
meta_message_id = fields.Char(string='WhatsApp Message ID (wamid)', index=True, copy=False)
error_message = fields.Text(string='Error Message')
retry_count = fields.Integer(string='Retry Count', default=0)
max_retries = fields.Integer(string='Max Retries', default=3)
last_attempt = fields.Datetime(string='Last Attempt')
company_id = fields.Many2one('res.company', string='Company', required=True,
default=lambda self: self.env.company)
@api.model
def send_order_notification(self, order, event_type, picking=None):
"""
Safe, non-blocking notification dispatcher for sale order events.
Enforces deduplication, template verification, phone validation, and asynchronous retry.
"""
if not order or not order.exists():
return False
company = order.company_id or self.env.company
if not company.whatsapp_enabled:
return False
# 1. Deduplication check: prevent sending the exact same event notification twice for this order
duplicate = self.search([
('sale_order_id', '=', order.id),
('event_type', '=', event_type),
('state', 'in', ('pending', 'sent', 'delivered', 'read'))
], limit=1)
if duplicate:
_logger.info("WhatsApp notification for %s event '%s' already sent (ID: %s). Skipping duplicate.",
order.name, event_type, duplicate.id)
return duplicate
# 2. Template verification: only send if approved template is configured and active
template = self.env['social.whatsapp.template'].search([
('event_type', '=', event_type),
('company_id', '=', company.id),
('is_active', '=', True)
], limit=1)
if not template:
_logger.warning("No active WhatsApp template configured for event '%s' in company %s. Notification skipped.",
event_type, company.name)
return False
# 3. Recipient phone number extraction & validation
raw_phone = order.partner_id.mobile or order.partner_id.phone
if not raw_phone:
_logger.warning("No phone number found on customer %s for order %s.", order.partner_id.name, order.name)
self.create({
'sale_order_id': order.id,
'picking_id': picking.id if picking else False,
'partner_id': order.partner_id.id,
'phone_number': 'MISSING',
'event_type': event_type,
'template_id': template.id,
'template_name': template.template_name,
'state': 'failed',
'error_message': _("Customer has no phone or mobile number."),
'company_id': company.id,
'last_attempt': fields.Datetime.now(),
})
return False
wa_api = self.env['social.whatsapp.api']
formatted_phone = wa_api.format_phone(raw_phone, partner=order.partner_id)
parameters = template.build_parameters(order, picking=picking)
# 4. Create pending notification record
notification = self.create({
'sale_order_id': order.id,
'picking_id': picking.id if picking else False,
'phone_number': formatted_phone,
'event_type': event_type,
'template_id': template.id,
'template_name': template.template_name,
'parameters_sent': ", ".join(parameters),
'state': 'pending',
'company_id': company.id,
'last_attempt': fields.Datetime.now(),
})
# 5. Dispatch via WhatsApp Cloud API
try:
result = wa_api.send_template_message(
company=company,
to_phone=formatted_phone,
template_name=template.template_name,
language_code=template.language_code,
parameters=parameters
)
now = fields.Datetime.now()
if result.get('success'):
notification.write({
'state': 'sent',
'meta_message_id': result.get('message_id'),
'error_message': False,
'last_attempt': now,
})
else:
notification.write({
'state': 'failed',
'error_message': result.get('error'),
'retry_count': 1,
'last_attempt': now,
})
except Exception as e:
_logger.exception("Unexpected error sending WhatsApp notification: %s", e)
notification.write({
'state': 'failed',
'error_message': str(e),
'retry_count': 1,
'last_attempt': fields.Datetime.now(),
})
return notification
def action_retry(self):
"""Action for administrators to retry failed notifications."""
wa_api = self.env['social.whatsapp.api']
for rec in self:
if rec.state != 'failed':
continue
if not rec.template_id:
rec.error_message = _("Template mapping no longer exists.")
continue
order = rec.sale_order_id
params = rec.template_id.build_parameters(order, picking=rec.picking_id)
result = wa_api.send_template_message(
company=rec.company_id,
to_phone=rec.phone_number,
template_name=rec.template_id.template_name,
language_code=rec.template_id.language_code,
parameters=params
)
now = fields.Datetime.now()
if result.get('success'):
rec.write({
'state': 'sent',
'meta_message_id': result.get('message_id'),
'error_message': False,
'last_attempt': now,
})
else:
rec.write({
'retry_count': rec.retry_count + 1,
'error_message': result.get('error'),
'last_attempt': now,
})
return True
@api.model
def retry_failed_notifications_cron(self, limit=20):
"""Cron job to automatically retry failed notifications."""
records = self.search([
('state', '=', 'failed'),
('retry_count', '<', 3),
], limit=limit)
if records:
_logger.info("Retrying %d failed WhatsApp notifications...", len(records))
records.action_retry()