Files
enterprise-addons/test_marketing_automation/tests/test_activity_triggers.py
T

1616 lines
74 KiB
Python

from datetime import timedelta
from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE
from odoo.addons.test_marketing_automation.tests.common import TestMACommon
from odoo.exceptions import ValidationError
from odoo.fields import Datetime
from odoo.tests import tagged, users
from odoo.tools import mute_logger
class ActivityTriggersCase(TestMACommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.date_reference = Datetime.from_string('2023-11-08 09:00:00')
# --------------------------------------------------
# TEST RECORDS, using marketing.test.sms (customers)
#
# 2 times
# - 3 records with partners
# - 1 records wo partner, but email/mobile
# - 1 record wo partner/email/mobile
# --------------------------------------------------
cls.test_records_base = cls._create_marketauto_records(model='marketing.test.sms', count=2)
cls.test_records = cls.test_records_base
cls.test_customers = cls.test_records.customer_id
cls.test_records_ko = cls.test_records_base[4] + cls.test_records_base[9]
cls.test_records_ok = cls.test_records - cls.test_records_ko
cls.campaign = cls.env['marketing.campaign'].create({
'domain': [('id', 'in', cls.test_records.ids)],
'model_id': cls.env['ir.model']._get_id('marketing.test.sms'),
'name': 'Test Campaign',
})
cls.test_mailing_mail = cls._create_mailing(
'marketing.test.sms',
email_from=cls.user_marketing_automation.email_formatted,
keep_archives=True,
mailing_type='mail',
user_id=cls.user_marketing_automation.id,
)
cls.test_mailing_sms = cls._create_mailing(
'marketing.test.sms',
mailing_type='sms',
user_id=cls.user_marketing_automation.id,
sms_force_send=True,
)
cls.test_wa_template = cls._create_wa_template(
'marketing.test.sms',
)
cls.test_sa_descr = cls.env['ir.actions.server'].create([
{
'code': f"""
for record in records:
record.write({{'description': (record.description or '') + ' - {description}'}})""",
'model_id': cls.env['ir.model']._get_id('marketing.test.sms'),
'name': f'Update {description}',
'state': 'code',
} for description in [
# mail
'mail_open', 'mail_not_open',
'mail_reply', 'mail_not_reply',
'mail_click', 'mail_not_click',
'mail_bounce',
# sms
'sms_click', 'sms_not_click',
'sms_bounce',
# whatsapp
'whatsapp_click', 'whatsapp_not_click',
'whatsapp_read', 'whatsapp_not_read',
'whatsapp_replied', 'whatsapp_not_replied',
'whatsapp_bounced',
# other
'action',
]
])
(
# mail
cls.test_sa_descr_mail_open, cls.test_sa_descr_mail_not_open,
cls.test_sa_descr_mail_reply, cls.test_sa_descr_mail_not_reply,
cls.test_sa_descr_mail_click, cls.test_sa_descr_mail_not_click,
cls.test_sa_descr_mail_bounce,
# sms
cls.test_sa_descr_sms_click, cls.test_sa_descr_sms_not_click,
cls.test_sa_descr_sms_bounce,
# whatsapp
cls.test_sa_descr_wa_click, cls.test_sa_descr_wa_not_click,
cls.test_sa_descr_wa_read, cls.test_sa_descr_wa_not_read,
cls.test_sa_descr_wa_replied, cls.test_sa_descr_wa_not_replied,
cls.test_sa_descr_wa_bounced,
# other
cls.test_sa_descr_action,
) = cls.test_sa_descr
cls.test_sa_unlink = cls.env['ir.actions.server'].create({
'code': "records.unlink()",
'model_id': cls.env['ir.model']._get_id('marketing.test.sms'),
'name': 'Unlink',
'state': 'code',
})
cls.env.flush_all()
@tagged('marketing_automation', 'ma_sync')
class TestActivityTriggers(ActivityTriggersCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
# begin activities: MAIL, SMS and WHATSAPP
cls.activity_begin_mail = cls._create_activity(
cls.campaign,
create_date=cls.date_reference,
mailing=cls.test_mailing_mail,
interval_number=1, interval_type='hours',
trigger_type='begin',
)
cls.activity_begin_sms = cls._create_activity(
cls.campaign,
create_date=cls.date_reference,
mailing=cls.test_mailing_sms,
interval_number=1, interval_type='hours',
trigger_type='begin',
)
cls.activity_begin_wa = cls._create_activity(
cls.campaign,
create_date=cls.date_reference,
wa_template=cls.test_wa_template,
interval_number=1, interval_type='hours',
trigger_type='begin',
)
cls.activity_begin_sa = cls._create_activity(
cls.campaign,
create_date=cls.date_reference,
action=cls.test_sa_descr_action,
interval_number=1, interval_type='hours',
trigger_type='begin',
)
(
# sub activities mail triggers
cls.activity_sa_mail_not_open,
cls.activity_sa_mail_open,
cls.activity_sa_mail_not_reply,
cls.activity_sa_mail_reply,
cls.activity_sa_mail_not_click,
cls.activity_sa_mail_click,
cls.activity_sa_mail_bounce,
# sub activities for sms triggers
cls.activity_sa_sms_not_click,
cls.activity_sa_sms_click,
cls.activity_sa_sms_bounce,
# sub activities for whatsapp triggers
cls.activity_sa_wa_not_read,
cls.activity_sa_wa_read,
cls.activity_sa_wa_not_replied,
cls.activity_sa_wa_replied,
cls.activity_sa_wa_not_click,
cls.activity_sa_wa_click,
cls.activity_sa_wa_bounced,
) = (
cls._create_activity(
cls.campaign,
action=server_action,
parent_id=parent.id,
interval_number=itv,
interval_type='days',
trigger_type=trigger_type,
)
for trigger_type, server_action, parent, itv in (
# mail
("mail_not_open", cls.test_sa_descr_mail_not_open, cls.activity_begin_mail, 5),
("mail_open", cls.test_sa_descr_mail_open, cls.activity_begin_mail, 1),
("mail_not_reply", cls.test_sa_descr_mail_not_reply, cls.activity_begin_mail, 5),
("mail_reply", cls.test_sa_descr_mail_reply, cls.activity_begin_mail, 1),
("mail_not_click", cls.test_sa_descr_mail_not_click, cls.activity_begin_mail, 5),
("mail_click", cls.test_sa_descr_mail_click, cls.activity_begin_mail, 1),
("mail_bounce", cls.test_sa_descr_mail_bounce, cls.activity_begin_mail, 1),
# sms
("sms_not_click", cls.test_sa_descr_sms_not_click, cls.activity_begin_sms, 5),
("sms_click", cls.test_sa_descr_sms_click, cls.activity_begin_sms, 1),
("sms_bounce", cls.test_sa_descr_sms_bounce, cls.activity_begin_sms, 1),
# whatsapp
("whatsapp_not_read", cls.test_sa_descr_wa_not_read, cls.activity_begin_wa, 5),
("whatsapp_read", cls.test_sa_descr_wa_read, cls.activity_begin_wa, 1),
("whatsapp_not_replied", cls.test_sa_descr_wa_not_replied, cls.activity_begin_wa, 5),
("whatsapp_replied", cls.test_sa_descr_wa_replied, cls.activity_begin_wa, 1),
("whatsapp_not_click", cls.test_sa_descr_wa_not_click, cls.activity_begin_wa, 5),
("whatsapp_click", cls.test_sa_descr_wa_click, cls.activity_begin_wa, 1),
("whatsapp_bounced", cls.test_sa_descr_wa_bounced, cls.activity_begin_wa, 1),
)
)
# sub activities other
cls.activity_mail_activity = cls._create_activity(
cls.campaign,
action=cls.test_sa_descr_action,
parent_id=cls.activity_begin_mail.id,
interval_number=2,
interval_type='hours',
trigger_type='activity',
)
cls.activity_sms_activity = cls._create_activity(
cls.campaign,
action=cls.test_sa_descr_action,
parent_id=cls.activity_begin_sms.id,
interval_number=2,
interval_type='hours',
trigger_type='activity',
)
cls.activity_wa_activity = cls._create_activity(
cls.campaign,
action=cls.test_sa_descr_action,
parent_id=cls.activity_begin_wa.id,
interval_number=2,
interval_type='hours',
trigger_type='activity',
)
cls.env.flush_all()
def test_assert_initial_values(self):
""" Check initial state, notably partners """
self.assertEqual(len(self.test_records), 10)
self.assertEqual(len(self.test_customers), 6)
self._launch_campaign(self.campaign, date_reference=self.date_reference)
for activity in (
self.activity_begin_mail + self.activity_begin_sms +
self.activity_begin_wa + self.activity_begin_sa
):
self.assertMarketAutoTraces(
[{
'records': self.test_records,
'status': 'scheduled',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
},
}],
activity,
)
@mute_logger('odoo.addons.mail.models.mail_mail', 'odoo.addons.mail.models.mail_thread')
@users('user_marketing_automation')
def test_reaction_triggers(self):
""" Test triggers (open / not_open, read / not read, ...) for mail, sms
and whatsapp. """
campaign = self.campaign.with_env(self.env)
test_records = self.test_records.with_env(self.env)
test_records_ok = self.test_records_ok.with_env(self.env)
test_records_ko = self.test_records_ko.with_env(self.env)
self._launch_campaign(campaign, date_reference=self.date_reference)
self.assertActivityWoTrace(
self.activity_sa_mail_open + self.activity_sa_mail_not_open +
self.activity_sa_mail_reply + self.activity_sa_mail_not_reply +
self.activity_sa_mail_click + self.activity_sa_mail_not_click +
self.activity_sa_mail_bounce +
self.activity_sa_sms_not_click + self.activity_sa_sms_click +
self.activity_sa_sms_bounce +
self.activity_sa_wa_not_read + self.activity_sa_wa_read +
self.activity_sa_wa_not_replied + self.activity_sa_wa_replied +
self.activity_sa_wa_not_click + self.activity_sa_wa_click +
self.activity_sa_wa_bounced +
self.activity_mail_activity + self.activity_sms_activity + self.activity_wa_activity
)
# First traces are processed, email, SMSes and WA msgs are sent (or failed)
date_send = self.date_reference + timedelta(hours=1) # ok for send mailing
date_opened = date_send + timedelta(hours=2) # simulating opened
date_not_action = date_send + timedelta(days=5) # 5 days delay before triggering 'did not reply' or similar
with self.mock_datetime_and_now(date_send), \
self.mockWhatsappGateway(), self.patchWhatsappCronTrigger(), \
self.mock_mail_gateway(), self.mockSMSGateway():
campaign.execute_activities()
self.assertMarketAutoTraces(
[{
'records': test_records_ok,
'records_to_partner': {r.id: r.customer_id for r in test_records_ok},
'status': 'processed',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
},
'trace_status': 'sent',
}, {
'records': test_records_ko,
'status': 'canceled',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
'state_msg': 'Email cancelled',
},
'trace_failure_type': 'mail_email_missing',
'trace_status': 'cancel',
}],
self.activity_begin_mail,
)
self.assertMarketAutoTraces(
[{
'records': test_records_ok,
'status': 'processed',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
},
'trace_status': 'pending', # forced send -> waiting provider update
}] + [{
'records': record,
'status': 'canceled',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
'state_msg': 'SMS cancelled',
},
'trace_content': f"Test SMS for {record.name} click on",
'trace_failure_type': 'sms_number_missing',
'trace_status': 'cancel',
} for record in test_records_ko],
self.activity_begin_sms,
)
self.assertMarketAutoTraces(
[{
'records': test_records_ok,
'status': 'processed',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
},
'trace_content': "<p>Hello your much wow template value</p>",
'trace_status': 'sent',
}] + [{
'records': record,
'status': 'error',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
'state_msg': 'WhatsApp failed',
},
'trace_content': "<p>Hello your much wow template value</p>",
'trace_failure_type': 'phone_invalid',
'trace_status': 'error',
} for record in test_records_ko],
self.activity_begin_wa,
)
self.assertMarketAutoTraces(
[{
'records': test_records,
'status': 'processed',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
},
}],
self.activity_begin_sa,
)
for sub_activity, schedule_date in [
# mail
(self.activity_sa_mail_open, False), # mail open: no date, as user input
(self.activity_sa_mail_not_open, date_not_action),
(self.activity_sa_mail_reply, False), # mail reply: no date, as user input
(self.activity_sa_mail_not_reply, date_not_action),
(self.activity_sa_mail_click, False), # mail click: no date, as user input
(self.activity_sa_mail_not_click, date_not_action),
(self.activity_sa_mail_bounce, False), # mail bounce: no date, as user input
# sms
(self.activity_sa_sms_click, False), # sms click: no date, as user input
(self.activity_sa_sms_not_click, date_not_action),
(self.activity_sa_sms_bounce, False), # sms bounce: no date, as user input
# whatsapp
(self.activity_sa_wa_read, False), # wa read: no date, as user input
(self.activity_sa_wa_not_read, date_not_action),
(self.activity_sa_wa_replied, False), # wa replied: no date, as user input
(self.activity_sa_wa_not_replied, date_not_action),
(self.activity_sa_wa_click, False), # wa click: no date, as user input
(self.activity_sa_wa_not_click, date_not_action),
(self.activity_sa_wa_bounced, False), # wa bounce: no date, as user input
# other
(self.activity_mail_activity, date_send + timedelta(hours=2)),
(self.activity_sms_activity, date_send + timedelta(hours=2)),
(self.activity_wa_activity, date_send + timedelta(hours=2)),
]:
self.assertMarketAutoTraces(
[{
'records': test_records,
'status': 'scheduled',
'fields_values': {
'schedule_date': schedule_date,
},
}],
sub_activity,
)
# Simulate open / reply / click
# - active traces should be processed, schedule date updated
# - opposite traces should be canceled
for action, action_sa, opposite_sa in (
# mail
("mail_open", self.activity_sa_mail_open, self.activity_sa_mail_not_open),
("mail_reply", self.activity_sa_mail_reply, self.activity_sa_mail_not_reply),
("mail_click", self.activity_sa_mail_click, self.activity_sa_mail_not_click),
# sms
("sms_click", self.activity_sa_sms_click, self.activity_sa_sms_not_click),
# whatsapp
("whatsapp_read", self.activity_sa_wa_read, self.activity_sa_wa_not_read),
("whatsapp_replied", self.activity_sa_wa_replied, self.activity_sa_wa_not_replied),
("whatsapp_click", self.activity_sa_wa_click, self.activity_sa_wa_not_click),
):
todo = test_records_ok[:5]
with self.mock_datetime_and_now(date_opened):
for record in todo:
if action == "mail_open":
self.gateway_mail_trace_open(self.activity_begin_mail.mass_mailing_id, record)
state_msg = 'Parent activity mail opened'
elif action == "mail_reply":
self.gateway_mail_trace_reply(MAIL_TEMPLATE, self.activity_begin_mail.mass_mailing_id, record)
state_msg = 'Parent activity mail replied'
elif action == "mail_click":
self.gateway_mail_trace_click(self.activity_begin_mail.mass_mailing_id, record, "LINK")
state_msg = 'Parent activity mail clicked'
elif action == "sms_click":
self.gateway_sms_click(self.activity_begin_sms.mass_mailing_id, record, use_sent_sms=False, sms_status='pending')
state_msg = 'Parent activity SMS clicked'
elif action == "whatsapp_read":
self.whatsapp_msg_read_with_records(record)
state_msg = 'Parent Whatsapp message got opened'
elif action == "whatsapp_replied":
self.whatsapp_answer_with_records(record, mock=False)
state_msg = 'Parent Whatsapp was replied to'
elif action == "whatsapp_click":
self.whatsapp_msg_click_with_records(record, button_index=0)
state_msg = 'Parent Whatsapp message was clicked'
# sub action scheduled
self.assertMarketAutoTraces(
[{
'records': todo,
'status': 'scheduled',
'fields_values': {
'schedule_date': date_opened + timedelta(days=1),
'state_msg': False,
},
}, {
'records': test_records - todo,
'status': 'scheduled',
'fields_values': {
'schedule_date': False,
},
}],
action_sa,
)
# opposition action is canceled
self.assertMarketAutoTraces(
[{
'records': todo,
'status': 'canceled',
'fields_values': {
'schedule_date': date_opened,
'state_msg': state_msg,
},
}, {
'records': test_records - todo,
'status': 'scheduled',
'fields_values': {
'schedule_date': date_not_action,
},
}],
opposite_sa,
)
@mute_logger('odoo.addons.mail.models.mail_mail')
@users('user_marketing_automation')
def test_reaction_triggers_bounce_fail(self):
""" Test specific 'bounce' and 'failed' triggers, that should
cancel a lot of things as if means contact is failing. """
campaign = self.campaign.with_env(self.env)
test_records = self.test_records.with_env(self.env)
test_records_ok = self.test_records_ok.with_env(self.env)
test_records_ko = self.test_records_ko.with_env(self.env)
bounced = test_records_ok[:2] # always post update
failed = test_records_ok[2:4] # fails at sending
post_failed = test_records_ok[4] # fails as post update
self._launch_campaign(campaign, date_reference=self.date_reference)
# First traces are processed, email, SMSes and WA msgs are sent (or failed)
# see 'test_reaction_triggers' that has the same beginning, with more asserts
date_send = self.date_reference + timedelta(hours=1) # ok for send mailing
date_opened = date_send + timedelta(hours=2) # simulating opened
with self.mock_datetime_and_now(date_send), \
self.mockWhatsappGateway(), self.patchWhatsappCronTrigger(), \
self.mock_mail_gateway(), self.mock_send_email_delivery_exception(failed.mapped('email_from'), error="OutboundSpamException"), \
self.mockSMSGateway(nbr_t_error={r.phone_sanitized: 'credit' for r in failed}):
campaign.execute_activities()
# Simulate bounce: should cancel all brother traces
for action, action_sa in (
# mail
("mail_bounce", self.activity_sa_mail_bounce),
# sms
("sms_bounce", self.activity_sa_sms_bounce),
# whatsapp
("whatsapp_bounced", self.activity_sa_wa_bounced),
):
with self.mock_datetime_and_now(date_opened):
for record in bounced:
if action == "mail_bounce":
self.gateway_mail_trace_bounce(self.activity_begin_mail.mass_mailing_id, record)
state_msg = 'Parent activity mail bounced'
elif action == "sms_bounce":
self.gateway_sms_bounce(self.activity_begin_sms.mass_mailing_id, record, error_code='invalid_destination')
state_msg = 'Parent activity SMS bounced'
elif action == "whatsapp_bounced":
self.whatsapp_msg_bounce_with_records(record, error_code=131026)
state_msg = 'Parent whatsapp was bounced'
# note that fail is not a trigger currently, hence do not change anything
for record in post_failed:
if action == "mail_bounce":
self.gateway_mail_trace_fail(self.activity_begin_mail.mass_mailing_id, record, failure_reason="Error without exception.", failure_type='unknown')
state_msg = 'Parent activity mail bounced'
if action == "sms_bounce":
self.gateway_sms_failed(self.activity_begin_sms.mass_mailing_id, record, error_code='not_delivered')
state_msg = 'Parent activity SMS bounced'
# TDE TODO: add other whatsapp reaction checks
self.assertMarketAutoTraces(
[{
'records': bounced,
'status': 'scheduled',
'fields_values': {
'schedule_date': date_opened + timedelta(days=1),
'state_msg': False,
},
}, {
'records': test_records - bounced,
'status': 'scheduled',
'fields_values': {
'schedule_date': False,
'state_msg': False,
},
}],
action_sa,
)
for brother in (action_sa.parent_id.child_ids - action_sa):
self.assertMarketAutoTraces(
[{
'records': bounced,
'status': 'canceled',
'fields_values': {
'schedule_date': date_opened,
'state_msg': state_msg,
},
}, {
'records': test_records - bounced,
'status': 'scheduled',
}],
brother,
)
# parent (mail / sms / WA sending) action update
self.assertMarketAutoTraces(
[{
'records': test_records_ok - (bounced + failed + post_failed),
'records_to_partner': {r.id: r.customer_id for r in (test_records_ok - (bounced + failed))},
'status': 'processed',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
'state_msg': False,
},
'trace_status': 'sent',
}, {
'records': failed,
'records_to_partner': {r.id: r.customer_id for r in failed},
'status': 'error',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
'state_msg': 'Email failed',
},
'mail_values': { # note that bounce did not update the mail.mail itself, only the trace
'failure_reason': "OutboundSpamException",
},
'trace_failure_reason': False, # FIXME: should be "OutboundSpamException"
'trace_failure_type': 'mail_spam',
'trace_status': 'error',
}, {
'records': post_failed,
'records_to_partner': {r.id: r.customer_id for r in post_failed},
'status': 'error',
'fields_values': {
'schedule_date': date_opened, # set_failed updated schedule_date to error dt, unsure this is right
'state_msg': 'SMS failed', # FIXME: well sms override is a bit too broad ^^
},
'mail_values': { # note that bounce did not update the mail.mail itself, only the trace
'failure_reason': "Error without exception.",
},
'trace_failure_reason': False, # FIXME: should be "Error without exception."
'trace_failure_type': 'unknown',
'trace_status': 'error',
}, {
'records': bounced,
'records_to_partner': {r.id: r.customer_id for r in bounced},
'status': 'processed',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
'state_msg': False,
},
'mail_values': { # note that bounce did not update the mail.mail itself, only the trace
'failure_reason': False,
'failure_type': False,
'state': 'sent',
},
'trace_failure_reason': 'This is the bounce email',
'trace_failure_type': 'mail_bounce',
'trace_status': 'bounce',
}, {
'records': test_records_ko,
'status': 'canceled',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
'state_msg': 'Email cancelled',
},
'trace_failure_type': 'mail_email_missing',
'trace_status': 'cancel',
}],
self.activity_begin_mail,
)
self.assertMarketAutoTraces(
[{
'records': (test_records_ok - (bounced + failed + post_failed)),
'status': 'processed',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
'state_msg': False,
},
'trace_status': 'pending', # forced send
}, {
'records': failed,
'status': 'error',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
'state_msg': 'SMS failed',
},
'trace_failure_reason': False,
'trace_failure_type': 'sms_credit',
'trace_status': 'error',
}, {
'records': post_failed,
'status': 'processed', # FIXME different from mail - asynchronous error update may explain it ?
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
'state_msg': False,
},
'sms_values': { # async fail does not update sms.sms values itself (generally unlinked anyway)
'failure_type': False,
'state': 'pending',
},
'trace_failure_reason': False,
'trace_failure_type': 'sms_not_delivered',
'trace_status': 'error',
}, {
'records': bounced,
'status': 'processed',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
'state_msg': False,
},
'sms_values': { # bounce does not update sms.sms values itself (generally unlinked anyway)
'failure_type': False,
'state': 'pending',
},
'trace_failure_reason': False, # FIXME nothing logged ?
'trace_failure_type': 'sms_invalid_destination',
'trace_status': 'bounce',
}, {
'records': test_records_ko,
'status': 'canceled',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
'state_msg': 'SMS cancelled',
},
'trace_failure_type': 'sms_number_missing',
'trace_status': 'cancel',
}],
self.activity_begin_sms,
)
self.assertMarketAutoTraces(
[{
'records': (test_records_ok - bounced),
'status': 'processed',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
},
'trace_content': "<p>Hello your much wow template value</p>",
'trace_status': 'sent',
}, {
'records': bounced,
'status': 'canceled', # FIXME: contrary to mail / sms, bounce = canceled
'fields_values': {
'schedule_date': date_opened, # update at failing dt
'state_msg': 'WhatsApp canceled',
},
'trace_failure_reason': '131026 : Message failed to send due to an unknown error.',
'trace_failure_type': 'whatsapp_unrecoverable',
'trace_status': 'bounce',
}, {
'records': test_records_ko,
'status': 'error',
'fields_values': {
'schedule_date': self.date_reference + timedelta(hours=1),
'state_msg': 'WhatsApp failed',
},
'trace_content': "<p>Hello your much wow template value</p>",
'trace_failure_type': 'phone_invalid',
'trace_status': 'error',
}],
self.activity_begin_wa,
)
@users('user_marketing_automation')
def test_reaction_triggers_multiple(self):
""" Test multiple triggers (e.g. several replies) do not trigger
child traces several times. """
campaign = self.campaign.with_env(self.env)
test_records = self.test_records.with_env(self.env)
test_records_ok = self.test_records_ok.with_env(self.env)
self.test_reaction_triggers()
date_opened = self.date_reference + timedelta(hours=1) + timedelta(hours=2) # see previous test
date_subaction = date_opened + timedelta(days=1)
# now that some records have been opened / replied / clicked, we can process
# sub activities linked to those triggers
with self.mock_datetime_and_now(date_subaction), \
self.mockWhatsappGateway(), self.patchWhatsappCronTrigger(), \
self.mock_mail_gateway(), self.mockSMSGateway():
campaign.execute_activities()
todo = test_records_ok[:5]
for subaction in (
# mail
self.activity_sa_mail_open, self.activity_sa_mail_reply, self.activity_sa_mail_click,
# sms
self.activity_sa_sms_click,
# whatsapp
self.activity_sa_wa_read, self.activity_sa_wa_replied, self.activity_sa_wa_click, self.activity_sa_wa_click,
):
with self.subTest(subaction_name=subaction.name):
self.assertMarketAutoTraces(
[{
'records': todo,
'status': 'processed',
'fields_values': {
'schedule_date': date_subaction,
'state_msg': False,
},
}, {
'records': test_records - todo,
'status': 'scheduled',
'fields_values': {
'schedule_date': False,
},
}],
subaction,
)
# receive replies again, check traces are still processed and not set
# back to schedule
# Simulate open / reply / click
# - active traces should be processed, schedule date updated
# - opposite traces should be canceled
for action, action_sa, opposite_sa in (
# mail
("mail_open", self.activity_sa_mail_open, self.activity_sa_mail_not_open),
("mail_reply", self.activity_sa_mail_reply, self.activity_sa_mail_not_reply),
("mail_click", self.activity_sa_mail_click, self.activity_sa_mail_not_click),
# sms
("sms_click", self.activity_sa_sms_click, self.activity_sa_sms_not_click),
# whatsapp
("whatsapp_read", self.activity_sa_wa_read, self.activity_sa_wa_not_read),
("whatsapp_replied", self.activity_sa_wa_replied, self.activity_sa_wa_not_replied),
# ("whatsapp_click", self.activity_sa_wa_click, self.activity_sa_wa_not_click), # tool not supporting it
):
todo = test_records_ok[:5]
with self.subTest(action=action):
with self.mock_datetime_and_now(date_opened):
for record in todo:
if action == "mail_open":
self.gateway_mail_trace_open(self.activity_begin_mail.mass_mailing_id, record)
elif action == "mail_reply":
self.gateway_mail_trace_reply(MAIL_TEMPLATE, self.activity_begin_mail.mass_mailing_id, record)
elif action == "mail_click":
self.gateway_mail_trace_click_simple(self.activity_begin_mail.mass_mailing_id, record)
elif action == "sms_click":
self.gateway_sms_click_simple(self.activity_begin_sms.mass_mailing_id, record)
elif action == "whatsapp_read":
self.whatsapp_msg_read_with_records(record, find_in_mock=False, mock=False)
elif action == "whatsapp_replied":
self.whatsapp_answer_with_records(record, find_in_mock=False, mock=False)
elif action == "whatsapp_click":
self.whatsapp_msg_click_with_records(record, button_index=0)
self.assertMarketAutoTraces(
[{
'records': todo,
'status': 'processed', # should not be back to scheduled
'fields_values': {
'schedule_date': date_subaction,
},
}, {
'records': test_records - todo,
'status': 'scheduled',
'fields_values': {
'schedule_date': False,
},
}],
action_sa,
)
self.assertMarketAutoTraces(
[{
'records': todo,
'status': 'canceled',
}, {
'records': test_records - todo,
'status': 'scheduled',
}],
opposite_sa,
)
@users('user_marketing_automation')
def test_schedule_date_based_on_triggers_new(self):
""" Test triggers when adding a new activity """
self._launch_campaign(self.campaign)
date_send = self.date_reference + timedelta(hours=1)
with self.mock_datetime_and_now(date_send), self.mock_mail_gateway():
self.campaign.execute_activities()
# add a new begin activity
new_activity = self._create_activity_mail(
self.campaign, self.env.user,
act_values={
"interval_number": 1,
"interval_type": "days",
"trigger_type": "begin",
},
)
# add new participants that should have traces for the begin activities
# so that we also check they are not duplicated at rescheduling
new_test_records = self._create_marketauto_records(model='marketing.test.sms', count=1)
self.campaign.write({"domain": [("id", "in", (self.test_records + new_test_records).ids)]})
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=3)):
self.campaign.sync_participants()
self.assertMarketAutoTraces(
[{
"records": new_test_records,
"status": "scheduled",
"fields_values": {
"schedule_date": self.date_reference + timedelta(days=1, hours=3),
},
}],
new_activity,
)
for activity in (
self.activity_begin_mail + self.activity_begin_sms +
self.activity_begin_wa + self.activity_begin_sa
):
self.assertMarketAutoTraces(
[{
"records": new_test_records,
"status": "scheduled",
"fields_values": {
"schedule_date": self.date_reference + timedelta(hours=4), # sync date + activity timedelta
},
}],
activity,
strict=False,
)
@mute_logger('odoo.addons.mail.models.mail_mail')
@users('user_marketing_automation')
def test_schedule_date_based_on_triggers_update(self):
""" Test triggers when updating activities """
self._launch_campaign(self.campaign, date_reference=self.date_reference)
date_send = self.date_reference + timedelta(hours=1)
date_subaction = date_send + timedelta(days=5)
with self.mock_datetime_and_now(date_send), self.mock_mail_gateway():
self.campaign.execute_activities()
# changing already-done activities: should not impact anything as child
# traces are based upon parent traces, already done
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=2)):
(
self.activity_begin_mail + self.activity_begin_sms +
self.activity_begin_wa + self.activity_begin_sa
).write({
"interval_number": 2,
"interval_type": "days",
})
self.campaign.action_update_participants()
for sub_activity, expected_schedule_date in [
# mail
(self.activity_sa_mail_not_open, date_subaction),
(self.activity_sa_mail_open, False), # mail open: no date, as user input
(self.activity_sa_mail_not_reply, date_subaction),
(self.activity_sa_mail_reply, False), # mail reply: no date, as user input
(self.activity_sa_mail_not_click, date_subaction),
(self.activity_sa_mail_click, False), # mail click: no date, as user input
(self.activity_sa_mail_bounce, False), # mail bounce: no date, as user input
# sms
(self.activity_sa_sms_not_click, date_subaction),
(self.activity_sa_sms_click, False), # sms click: no date, as user input
(self.activity_sa_sms_bounce, False), # sms bounce: no date, as user input
# whatsapp
(self.activity_sa_wa_not_read, date_subaction),
(self.activity_sa_wa_read, False), # wa read: no date, as user input
(self.activity_sa_wa_not_replied, date_subaction),
(self.activity_sa_wa_replied, False), # wa replied: no date, as user input
(self.activity_sa_wa_not_click, date_subaction),
(self.activity_sa_wa_click, False), # wa click: no date, as user input
(self.activity_sa_wa_bounced, False), # wa bounce: no date, as user input
# other
(self.activity_mail_activity, date_send + timedelta(hours=2)),
(self.activity_sms_activity, date_send + timedelta(hours=2)),
(self.activity_wa_activity, date_send + timedelta(hours=2)),
]:
self.assertMarketAutoTraces(
[{
'records': self.test_records,
'status': 'scheduled',
'fields_values': {
'schedule_date': expected_schedule_date,
},
}],
sub_activity,
)
# change scheduled activities: should not impact "no deadline" activities
# such as mail_open, mail_click as they depend on user action
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=3)):
(
# mail
self.activity_sa_mail_not_open + self.activity_sa_mail_open +
self.activity_sa_mail_not_reply + self.activity_sa_mail_reply +
self.activity_sa_mail_not_click + self.activity_sa_mail_click +
self.activity_sa_mail_bounce +
# sms
self.activity_sa_sms_not_click + self.activity_sa_sms_click +
self.activity_sa_sms_bounce +
# whatsapp
self.activity_sa_wa_not_read + self.activity_sa_wa_read +
self.activity_sa_wa_not_replied + self.activity_sa_wa_replied +
self.activity_sa_wa_not_click + self.activity_sa_wa_click +
self.activity_sa_wa_bounced +
# other
self.activity_mail_activity + self.activity_sms_activity + self.activity_wa_activity
).write({
"interval_number": 2,
"interval_type": "days",
})
self.campaign.action_update_participants()
date_subaction = date_send + timedelta(days=2)
for sub_activity, expected_schedule_date in [
# mail
(self.activity_sa_mail_not_open, date_subaction),
(self.activity_sa_mail_open, False), # mail open: no date, as user input
(self.activity_sa_mail_not_reply, date_subaction),
(self.activity_sa_mail_reply, False), # mail reply: no date, as user input
(self.activity_sa_mail_not_click, date_subaction),
(self.activity_sa_mail_click, False), # mail click: no date, as user input
(self.activity_sa_mail_bounce, False), # mail bounce: no date, as user input
# sms
(self.activity_sa_sms_not_click, date_subaction),
(self.activity_sa_sms_click, False), # sms click: no date, as user input
(self.activity_sa_sms_bounce, False), # sms bounce: no date, as user input
# whatsapp
(self.activity_sa_wa_not_read, date_subaction),
# (self.activity_sa_wa_read, False), # wa read: no date, as user input
(self.activity_sa_wa_not_replied, date_subaction),
# (self.activity_sa_wa_replied, False), # wa reply: no date, as user input
(self.activity_sa_wa_not_click, date_subaction),
(self.activity_sa_wa_click, False), # wa click: no date, as user input
(self.activity_sa_wa_bounced, False), # wa bounce: no date, as user input
# other
(self.activity_mail_activity, date_subaction),
(self.activity_sms_activity, date_subaction),
(self.activity_wa_activity, date_subaction),
]:
self.assertMarketAutoTraces(
[{
'records': self.test_records,
'status': 'scheduled',
'fields_values': {
'schedule_date': expected_schedule_date,
},
}],
sub_activity,
)
# update write_date on some traces by triggering trace update
new_now = self.date_reference + timedelta(hours=5)
date_subaction_new = new_now + timedelta(days=2)
with self.mock_datetime_and_now(new_now):
self.gateway_mail_trace_open(self.activity_begin_mail.mass_mailing_id, self.test_records[0])
self.gateway_mail_trace_reply(MAIL_TEMPLATE, self.activity_begin_mail.mass_mailing_id, self.test_records[1])
self.gateway_mail_trace_click(self.activity_begin_mail.mass_mailing_id, self.test_records[2], "LINK")
for record, sub_activity, exp_status, exp_schedule_date in [
# mail
(self.test_records[0], self.activity_sa_mail_not_open, "canceled", new_now),
(self.test_records[0], self.activity_sa_mail_open, "scheduled", date_subaction_new),
]:
self.assertMarketAutoTraces(
[{
'records': record,
'status': exp_status,
'fields_values': {
'schedule_date': exp_schedule_date,
},
}],
sub_activity,
strict=False,
)
# update sub actions, updated date should be based on trace date
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=3)):
(
# mail
self.activity_sa_mail_not_open + self.activity_sa_mail_open +
self.activity_sa_mail_not_reply + self.activity_sa_mail_reply +
self.activity_sa_mail_not_click + self.activity_sa_mail_click +
self.activity_sa_mail_bounce +
# sms
self.activity_sa_sms_not_click + self.activity_sa_sms_click +
self.activity_sa_sms_bounce +
# whatsapp
self.activity_sa_wa_not_read + self.activity_sa_wa_read +
self.activity_sa_wa_not_replied + self.activity_sa_wa_replied +
self.activity_sa_wa_not_click + self.activity_sa_wa_click +
self.activity_sa_wa_bounced
).write({
"interval_number": 5,
"interval_type": "days",
})
self.campaign.action_update_participants()
date_subaction_upd = new_now + timedelta(days=5)
for record, sub_activity, exp_status, exp_schedule_date in [
# opened mail
(self.test_records[0], self.activity_sa_mail_not_open, "canceled", new_now),
(self.test_records[0], self.activity_sa_mail_open, "scheduled", date_subaction_upd),
(self.test_records[0], self.activity_sa_mail_not_reply, "scheduled", date_subaction_upd - timedelta(hours=4)), # still cheduled on opened mail, also unsure those hours ??
(self.test_records[0], self.activity_sa_mail_reply, "scheduled", False), # user input, should not be scheduled
# replied mail
(self.test_records[1], self.activity_sa_mail_not_open, "canceled", new_now),
(self.test_records[1], self.activity_sa_mail_open, "scheduled", date_subaction_upd), # mail open: shoudln't it be no date, as user input ?
(self.test_records[1], self.activity_sa_mail_not_reply, "canceled", new_now),
(self.test_records[1], self.activity_sa_mail_reply, "scheduled", date_subaction_upd), # date_subaction_new ?
]:
self.assertMarketAutoTraces(
[{
'records': record,
'status': exp_status,
'fields_values': {
'schedule_date': exp_schedule_date,
},
}],
sub_activity,
strict=False,
)
@users('user_marketing_automation')
def test_updating_activity_hierarchy(self):
""" Test activity hierarchy updates are restricted when traces exist """
self._launch_campaign(self.campaign, date_reference=self.date_reference)
date_send = self.date_reference + timedelta(hours=1)
self.assertTrue(self.activity_begin_sms.trace_ids)
with self.assertRaises(ValidationError):
self.activity_begin_sms.write({
"trigger_type": "activity",
"parent_id": self.activity_begin_mail,
})
self.assertFalse(self.activity_mail_activity.trace_ids)
with self.mock_datetime_and_now(date_send):
self.activity_mail_activity.write({
"trigger_type": "activity",
"parent_id": self.activity_begin_sms,
})
self.activity_begin_sms.execute()
self.assertMarketAutoTraces(
[{
"records": self.test_records,
"status": "scheduled",
"fields_values": {
"schedule_date": date_send + timedelta(hours=2),
},
}],
self.activity_mail_activity,
strict=True,
)
@users('user_marketing_automation')
def test_updating_activity_parent_in_test(self):
""" Test activity hierarchy updates with test traces """
self.campaign.domain = [(0, '=', 1)]
self._launch_campaign(self.campaign, date_reference=self.date_reference)
self.env['marketing.campaign.test'].create({
'campaign_id': self.campaign.id,
'res_id': self.test_records[0].id,
}).action_launch_test()
self.assertEqual(len(self.activity_begin_sms.trace_ids), 1)
test_trace = self.activity_begin_sms.trace_ids[0]
self.assertEqual(test_trace.state, 'scheduled')
self.activity_begin_sms.write({
'trigger_type': 'activity',
'parent_id': self.activity_begin_mail,
})
self.activity_begin_sms.write({
'interval_number': 5,
'interval_type': 'days',
})
self.assertTrue(self.activity_begin_sms.require_sync)
self.campaign.action_update_participants()
expected_date = test_trace.participant_id.create_date + timedelta(days=5)
self.assertEqual(test_trace.schedule_date, expected_date)
@tagged('marketing_automation', 'ma_sync')
class TestSynchronize(ActivityTriggersCase):
@mute_logger('odoo.addons.mail.models.mail_thread')
@users('user_marketing_automation')
def test_post_launch_synchronize(self):
""" Test participants creation then update when adding new activities
on a running campaign. We should not duplicate traces, notably when
a participant had traces on a new activity that is then updated
automatically. """
campaign = self.env['marketing.campaign'].create({
'domain': [('id', 'in', self.test_records.ids)],
'model_id': self.env['ir.model']._get_id('marketing.test.sms'),
'name': 'Test Campaign',
})
# begin activities, parents for each type category to test
activity_begin_mail = self._create_activity(
campaign,
create_date=self.date_reference,
mailing=self.test_mailing_mail,
interval_number=1, interval_type='hours',
trigger_type='begin',
)
activity_begin_sms = self._create_activity(
campaign,
create_date=self.date_reference,
mailing=self.test_mailing_sms,
interval_number=1, interval_type='hours',
trigger_type='begin',
)
activity_begin_wa = self._create_activity(
campaign,
create_date=self.date_reference,
wa_template=self.test_wa_template,
interval_number=1, interval_type='hours',
trigger_type='begin',
)
_activity_begin_sa = self._create_activity(
campaign,
create_date=self.date_reference,
action=self.test_sa_descr_action,
interval_number=1, interval_type='hours',
trigger_type='begin',
)
# init participants and traces
with self.mock_datetime_and_now(self.date_reference):
self._launch_campaign(campaign, self.date_reference)
campaign.action_update_participants()
# run begin activities so that we have traces for replies / ...
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=1)), \
self.mockWhatsappGateway(), self.patchWhatsappCronTrigger(), \
self.mock_mail_gateway(), self.mockSMSGateway():
campaign.execute_activities()
for parent, existing, additional, exp_assert_values in (
# mail: records 0-1 are open, 2-3 are replied to, 6-7 are clicked, 8 is bounced
# ------------------------------------------------------------
(
activity_begin_mail,
("mail_not_open", self.test_sa_descr_mail_not_open, 0),
(
("mail_open", self.test_sa_descr_mail_open),
("mail_reply", self.test_sa_descr_mail_reply),
),
(
{
'records': self.test_records_ok, # open still holds, unsure
}, {
'records': self.test_records_ok, # reply still holds, unsure
},
),
),
(
activity_begin_mail,
("mail_open", self.test_sa_descr_mail_open, 0),
(
("mail_not_open", self.test_sa_descr_mail_not_open),
("mail_not_reply", self.test_sa_descr_mail_not_reply),
),
(
{
'records': self.test_records_ok[4] + self.test_records_ok[7:], # not_open canceled by open - note that reply and click implies open
}, {
'records': self.test_records_ok, # not_reply still holds even if opened
},
),
),
(
activity_begin_mail,
("mail_not_reply", self.test_sa_descr_mail_not_reply, 0),
(
("mail_open", self.test_sa_descr_mail_open),
("mail_reply", self.test_sa_descr_mail_reply),
),
(
{
'records': self.test_records_ok, # open still holds, unsure
}, {
'records': self.test_records_ok, # reply still holds, unsure
},
),
),
(
activity_begin_mail,
("mail_reply", self.test_sa_descr_mail_reply, 0),
(
("mail_not_open", self.test_sa_descr_mail_not_open),
("mail_not_reply", self.test_sa_descr_mail_not_reply),
),
(
{
'records': self.test_records_ok, # not_open still holds even if replied (to fix probably)
}, {
'records': self.test_records_ok[:2] + self.test_records_ok[4:], # not_reply canceled by reply
},
),
),
(
activity_begin_mail,
("mail_not_click", self.test_sa_descr_mail_not_click, 0),
(
("mail_open", self.test_sa_descr_mail_open),
("mail_reply", self.test_sa_descr_mail_reply),
),
(
{
'records': self.test_records_ok, # open still holds, unsure
}, {
'records': self.test_records_ok, # reply still holds, unsure
},
),
),
(
activity_begin_mail,
("mail_click", self.test_sa_descr_mail_click, 0),
(
("mail_not_open", self.test_sa_descr_mail_not_open),
("mail_not_reply", self.test_sa_descr_mail_not_reply),
),
(
{
'records': self.test_records_ok, # not_open still holds even if clicked (to fix probably)
}, {
'records': self.test_records_ok, # not_reply still holds
},
),
),
(
activity_begin_mail,
("mail_bounce", self.test_sa_descr_mail_bounce, 0),
(
("mail_open", self.test_sa_descr_mail_open),
("mail_not_open", self.test_sa_descr_mail_not_open),
),
(
{
'records': self.test_records_ok, # open still holds even if bounced (to check)
}, {
'records': self.test_records_ok, # not_reply still holds
},
),
),
# sms
# ------------------------------------------------------------
(
activity_begin_sms,
("sms_not_click", self.test_sa_descr_sms_not_click, 0),
(
("sms_click", self.test_sa_descr_sms_click),
),
(
{
'records': self.test_records_ok, # click still holds, unsure
},
),
),
(
activity_begin_sms,
("sms_click", self.test_sa_descr_sms_click, 0),
(
("sms_not_click", self.test_sa_descr_sms_not_click),
),
(
{
'records': self.test_records_ok[2:], # not_click canceled by click
},
),
),
(
activity_begin_sms,
("sms_bounce", self.test_sa_descr_sms_bounce, 0),
(
("sms_click", self.test_sa_descr_sms_click),
("sms_not_click", self.test_sa_descr_sms_not_click),
),
(
{
'records': self.test_records_ok, # check me
}, {
'records': self.test_records_ok, # check me
},
),
),
# whatsapp
# ------------------------------------------------------------
(
activity_begin_wa,
("whatsapp_not_read", self.test_sa_descr_wa_not_read, 0),
(
("whatsapp_read", self.test_sa_descr_wa_read),
),
(
{
'records': self.test_records_ok, # read still holds, unsure
},
),
),
(
activity_begin_wa,
("whatsapp_read", self.test_sa_descr_wa_read, 0),
(
("whatsapp_not_read", self.test_sa_descr_wa_not_read),
),
(
{
'records': self.test_records_ok[2:], # not_read canceled by read
},
),
),
(
activity_begin_wa,
("whatsapp_not_replied", self.test_sa_descr_wa_not_replied, 0),
(
("whatsapp_read", self.test_sa_descr_wa_read),
("whatsapp_replied", self.test_sa_descr_wa_replied),
),
(
{
'records': self.test_records_ok, # read still holds, unsure
}, {
'records': self.test_records_ok, # reply still holds, unsure
},
),
),
(
activity_begin_wa,
("whatsapp_replied", self.test_sa_descr_wa_replied, 0),
(
("whatsapp_not_read", self.test_sa_descr_wa_not_read),
("whatsapp_not_replied", self.test_sa_descr_wa_not_replied),
),
(
{
'records': self.test_records_ok, # not_read still holds even if replied (to fix probably)
}, {
'records': self.test_records_ok, # FIXME: not_replied should be canceled by read
},
),
),
(
activity_begin_wa,
("whatsapp_not_click", self.test_sa_descr_wa_not_click, 0),
(
("whatsapp_read", self.test_sa_descr_wa_read),
("whatsapp_replied", self.test_sa_descr_wa_replied),
),
(
{
'records': self.test_records_ok, # open still holds, unsure
}, {
'records': self.test_records_ok, # replied still holds, unsure
},
),
),
(
activity_begin_wa,
("whatsapp_click", self.test_sa_descr_wa_click, 0),
(
("whatsapp_not_read", self.test_sa_descr_wa_not_read),
("whatsapp_not_replied", self.test_sa_descr_wa_not_replied),
),
(
{
'records': self.test_records_ok, # not_read still holds even if replied (to fix probably)
}, {
'records': self.test_records_ok, # not_replied still holds, logic (clicked, not necessarily replied)
},
),
),
(
activity_begin_wa,
("whatsapp_bounced", self.test_sa_descr_wa_bounced, 0),
(
("whatsapp_read", self.test_sa_descr_wa_read),
("whatsapp_not_read", self.test_sa_descr_wa_not_read),
("whatsapp_replied", self.test_sa_descr_wa_replied),
("whatsapp_not_replied", self.test_sa_descr_wa_not_replied),
),
(
{
'records': self.test_records_ok, # read still holds even if bounced (to fix probably)
}, {
'records': self.test_records_ok, # not_read still holds, unsure
}, {
'records': self.test_records_ok, # replied still holds even if bounced (to fix probably)
}, {
'records': self.test_records_ok, # not_replied still holds, unsure
},
),
),
):
trigger_type, server_action, interval = existing
with self.subTest(parent_name=parent.name, trigger_type=trigger_type):
# create activity, schedule it to have traces
new_activities = self._create_activity(
campaign,
create_date=self.date_reference + timedelta(hours=1),
action=server_action,
interval_number=interval, interval_type='days',
parent_id=parent.id,
trigger_type=trigger_type,
)
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=2)):
campaign.action_update_participants() # scheduled newly create activity
if parent.activity_type == 'email':
# update traces: open
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
self.gateway_mail_trace_open(parent.mass_mailing_id, self.test_records[0:2])
# update traces: reply
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
self.gateway_mail_trace_reply(MAIL_TEMPLATE, parent.mass_mailing_id, self.test_records[2:4])
# update traces: click
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
self.gateway_mail_trace_click(parent.mass_mailing_id, self.test_records[6:8], "LINK")
# update traces: bounce
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
self.gateway_mail_trace_bounce(parent.mass_mailing_id, self.test_records[8])
elif parent.activity_type == 'sms':
# update traces: click
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
self.gateway_sms_click(parent.mass_mailing_id, self.test_records[0], use_sent_sms=False, sms_status='pending')
self.gateway_sms_click(parent.mass_mailing_id, self.test_records[1], use_sent_sms=False, sms_status='pending')
# update traces: bounce
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
self.gateway_sms_bounce(parent.mass_mailing_id, self.test_records[2])
elif parent.activity_type == 'whatsapp':
# update traces: open
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=4)):
self.whatsapp_msg_read_with_records(self.test_records[0:2])
else:
raise NotImplementedError()
# add brother-like activity, check status
for to_create in additional:
trigger_type, server_action = to_create
new_activities += self._create_activity(
campaign,
create_date=self.date_reference + timedelta(hours=5),
action=server_action,
interval_number=1, interval_type='days',
parent_id=parent.id,
trigger_type=trigger_type,
)
with self.mock_datetime_and_now(self.date_reference + timedelta(hours=8)):
campaign.action_update_participants()
# final expected status: for parent
if parent.activity_type == 'email':
self.assertMarketAutoTraces( # begin mail activity sent for everyone
[{
'records': self.test_records_ok[0:2],
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[0:2]},
'status': 'processed',
'trace_status': 'open', # gateway trigerred
}, {
'records': self.test_records_ok[2:4],
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[2:4]},
'status': 'processed',
'trace_status': 'reply', # gateway trigerred
}, {
'records': self.test_records_ok[5:7],
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[5:7]},
'mailing_trace_values': {
'links_click_datetime': self.date_reference + timedelta(hours=4),
},
'status': 'processed',
'trace_status': 'open', # click is not a status
}, {
'records': self.test_records_ok[7],
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[7]},
'status': 'processed',
'mail_values': { # note that bounce did not update the mail.mail itself, only the trace
'failure_reason': False,
'failure_type': False,
'state': 'sent',
},
'trace_failure_reason': 'This is the bounce email',
'trace_failure_type': 'mail_bounce',
'trace_status': 'bounce', # click is not a status
}, {
'records': self.test_records_ok[4] + self.test_records_ok[8:],
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[4] + self.test_records_ok[8:]},
'status': 'processed',
'trace_status': 'sent',
}, {
'records': self.test_records_ko,
'status': 'canceled',
'trace_failure_type': 'mail_email_missing',
'trace_status': 'cancel',
}],
parent,
)
elif parent.activity_type == 'sms':
self.assertMarketAutoTraces( # begin sms activity sent for everyone
[{
'records': self.test_records_ok[0:2],
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[0:2]},
'mailing_trace_values': {
'links_click_datetime': self.date_reference + timedelta(hours=4),
},
'sms_values': {
'state': 'pending', # sms_force_send activated, hence not ouggoing
},
'status': 'processed',
'trace_status': 'open', # click is not a status
}, {
'check_sms': False, # sms untouched, still old values (not bounced)
'records': self.test_records_ok[2],
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[2]},
'status': 'processed',
'trace_failure_type': 'sms_invalid_destination',
'trace_status': 'bounce',
}, {
'records': self.test_records_ok[3:],
'records_to_partner': {r.id: r.customer_id for r in self.test_records_ok[2:4]},
'status': 'processed',
'trace_status': 'pending', # sms_force_send activated, hence not outgoing
}, {
'records': self.test_records_ko,
'status': 'canceled',
'trace_failure_type': 'sms_number_missing',
'trace_status': 'cancel',
}],
parent,
)
elif parent.activity_type == 'whatsapp':
self.assertMarketAutoTraces( # begin wa activity sent for everyone
[{
'records': self.test_records_ok[0:2],
'status': 'processed',
'trace_status': 'read', # trace_status is actually directly wa message state here
}, {
'records': self.test_records_ok[2:],
'status': 'processed',
'trace_status': 'sent', # trace_status is actually directly wa message state here
}, {
'records': self.test_records_ko,
'status': 'error',
'trace_failure_type': 'phone_invalid',
'trace_status': 'error',
}],
parent,
)
# final expected status: for children
for new_activity, check_values in zip(new_activities[1:], exp_assert_values, strict=True):
with self.subTest(new_activity_name=new_activity.name):
self.assertMarketAutoTraces(
[{
'records': self.test_records_ok,
'status': 'scheduled',
**check_values,
}],
new_activity,
)
# cleanup
new_activities.unlink()
campaign.last_sync_date = self.date_reference