1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/test_marketing_automation/tests/test_flow.py
2024-12-10 09:04:09 +07:00

574 lines
24 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from odoo.addons.base.tests.test_ir_cron import CronMixinCase
from odoo.addons.test_mail.data.test_mail_data import MAIL_TEMPLATE
from odoo.addons.test_marketing_automation.tests.common import TestMACommon
from odoo.fields import Datetime
from odoo.tests import tagged, users
from odoo.tools import mute_logger
@tagged('marketing_automation')
class TestMarketAutoFlow(TestMACommon, CronMixinCase):
@classmethod
def setUpClass(cls):
super(TestMarketAutoFlow, cls).setUpClass()
cls.date_reference = Datetime.from_string('2014-08-01 15:02:32') # so long, little task
cls.env['res.lang']._activate_lang('fr_FR')
# --------------------------------------------------
# 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
# 1 wrong email
# 1 duplicate)
# --------------------------------------------------
cls.test_records_base = cls._create_marketauto_records(model='marketing.test.sms', count=2)
cls.test_records_failure = cls.env['marketing.test.sms'].create([
{
'email_from': 'wrong',
'mobile': '0455990000',
'name': 'Wrong Email',
}, {
'email_from': cls.test_records_base[1].email_from,
'mobile': cls.test_records_base[1].mobile,
# compared to < 17, we need the name to be the same, as duplicate
# comparison is now done on sent content + recipient, not just
# the recipient itself
'name': cls.test_records_base[1].name,
},
])
(cls.test_records_failure_wrong, cls.test_records_failure_dupe) = cls.test_records_failure
cls.test_records = cls.test_records_base + cls.test_records_failure
# --------------------------------------------------
# CAMPAIGN, based on marketing.test.sms (customers)
#
# ACT1 MAIL mailing
# ACT2.1 -> reply -> send an SMS after 1h with a promotional link
# ACT3.1 -> sms_click -> send a confirmation SMS right at click
# ACT2.2 -> not opened within 1 day-> update description through server action
# --------------------------------------------------
cls.campaign = cls.env['marketing.campaign'].with_user(cls.user_marketing_automation).create({
'domain': [('name', '!=', 'Invalid')],
'model_id': cls.env['ir.model']._get_id('marketing.test.sms'),
'name': 'Test Campaign',
})
# first activity: send a mailing
cls.act1_mailing = cls._create_mailing(
'marketing.test.sms',
email_from=cls.user_marketing_automation.email_formatted,
keep_archives=True,
).with_user(cls.user_marketing_automation)
cls.act1 = cls._create_activity(
cls.campaign,
mailing=cls.act1_mailing,
trigger_type='begin',
interval_number=0,
).with_user(cls.user_marketing_automation)
# second activity: send an SMS 1 hour after a reply
cls.act2_1_mailing = cls._create_mailing(
'marketing.test.sms',
mailing_type='sms',
body_plaintext='SMS for {{ object.name }}: mega promo on https://test.example.com',
sms_allow_unsubscribe=True,
).with_user(cls.user_marketing_automation)
cls.act2_1 = cls._create_activity(
cls.campaign,
mailing=cls.act2_1_mailing,
parent_id=cls.act1.id,
trigger_type='mail_reply',
interval_number=1, interval_type='hours',
).with_user(cls.user_marketing_automation)
# other activity: update description if not opened after 1 day
# created by admin, should probably not give rights to marketing
cls.act2_2_sact = cls.env['ir.actions.server'].create({
'code': """
for record in records:
record.write({'description': record.description + ' - Did not answer, sad campaign is sad.'})""",
'model_id': cls.env['ir.model']._get('marketing.test.sms').id,
'name': 'Update description',
'state': 'code',
})
cls.act2_2 = cls._create_activity(
cls.campaign,
action=cls.act2_2_sact,
parent_id=cls.act1.id,
trigger_type='mail_not_open',
interval_number=1, interval_type='days',
activity_domain=[('email_from', '!=', False)],
).with_user(cls.user_marketing_automation)
cls.act3_1_mailing = cls._create_mailing(
'marketing.test.sms',
mailing_type='sms',
body_plaintext='Confirmation for {{ object.name }}',
sms_allow_unsubscribe=False,
).with_user(cls.user_marketing_automation)
cls.act3_1 = cls._create_activity(
cls.campaign,
mailing=cls.act3_1_mailing,
parent_id=cls.act2_1.id,
trigger_type='sms_click',
interval_number=0,
).with_user(cls.user_marketing_automation)
cls.env.flush_all()
def test_assert_initial_values(self):
""" Test initial values to have a common ground for other tests """
# ensure initial data
self.assertEqual(len(self.test_records), 12)
self.assertEqual(len(self.test_records.filtered(lambda r: r.name != 'Test_00')), 11)
self.assertEqual(self.campaign.state, 'draft')
@mute_logger('odoo.addons.base.models.ir_model',
'odoo.addons.mail.models.mail_mail',
'odoo.addons.mass_mailing.models.mailing',
'odoo.addons.mass_mailing_sms.models.mailing_mailing')
@users('user_marketing_automation')
def test_marketing_automation_flow(self):
""" Test a marketing automation flow involving several steps. """
# init test variables to ease code reading
date_reference = self.date_reference
test_records = self.test_records.with_user(self.env.user)
test_records_init = test_records.filtered(lambda r: r.name != 'Test_00')
# update campaign
act1 = self.act1.with_user(self.env.user)
act2_1 = self.act2_1.with_user(self.env.user)
act2_2 = self.act2_2.with_user(self.env.user)
act3_1 = self.act3_1.with_user(self.env.user)
campaign = self.campaign.with_user(self.env.user)
campaign.write({
'domain': [('name', '!=', 'Test_00')],
})
# CAMPAIGN START
# ------------------------------------------------------------
# User starts and syncs its campaign
with freeze_time(self.date_reference), \
self.capture_triggers('marketing_automation.ir_cron_campaign_sync_participants') as captured_triggers:
campaign.action_start_campaign()
self.assertEqual(campaign.state, 'running')
# a cron.trigger has been created to sync participants after campaign start
self.assertEqual(len(captured_triggers.records), 1)
self.assertEqual(
captured_triggers.records[0].cron_id,
self.env.ref('marketing_automation.ir_cron_campaign_sync_participants'))
self.assertEqual(captured_triggers.records[0].call_at, self.date_reference)
with freeze_time(date_reference), \
self.capture_triggers('marketing_automation.ir_cron_campaign_execute_activities') as captured_triggers:
campaign.sync_participants()
# All records not containing Test_00 should be added as participants
self.assertEqual(campaign.running_participant_count, len(test_records_init))
self.assertEqual(
set(campaign.participant_ids.mapped('res_id')),
set(test_records_init.ids)
)
self.assertEqual(
set(campaign.participant_ids.mapped('state')),
set(['running'])
)
# Beginning activity should contain a scheduled trace for each participant
self.assertMarketAutoTraces(
[{
'status': 'scheduled',
'records': test_records_init,
'participants': campaign.participant_ids,
'fields_values': {
'schedule_date': date_reference,
},
}],
act1,
)
# a cron.trigger has been created to execute activities after campaign start
# there should only be one since we have 9 activities with the same scheduled_date
self.assertEqual(len(captured_triggers.records), 1)
self.assertEqual(
captured_triggers.records[0].cron_id,
self.env.ref('marketing_automation.ir_cron_campaign_execute_activities'))
self.assertEqual(captured_triggers.records[0].call_at, self.date_reference)
# No other trace should have been created as the first one are waiting to be processed
self.assertEqual(act2_1.trace_ids, self.env['marketing.trace'])
self.assertEqual(act2_2.trace_ids, self.env['marketing.trace'])
self.assertEqual(act3_1.trace_ids, self.env['marketing.trace'])
# ACT1: LAUNCH MAILING
# ------------------------------------------------------------
test_records_1_ko = test_records_init.filtered(
lambda r: not r.email_from or r.email_from == 'wrong'
) + self.test_records_failure_dupe
test_records_1_ok = test_records_init.filtered(lambda r: r not in test_records_1_ko)
# First traces are processed, emails are sent (or failed)
with freeze_time(self.date_reference), \
self.mock_mail_gateway(), \
self.capture_triggers('marketing_automation.ir_cron_campaign_execute_activities') as captured_triggers:
campaign.execute_activities()
self.assertMarketAutoTraces(
[{
'status': 'processed',
'records': test_records_1_ok,
'trace_status': 'sent',
'fields_values': {
'schedule_date': date_reference,
},
}, {
'status': 'canceled',
'records': self.test_records_failure_wrong,
'fields_values': {
'schedule_date': date_reference,
'state_msg': 'Email canceled',
},
# wrong email -> trace set as ignored
'trace_email': self.test_records_failure_wrong.email_from,
'trace_failure_type': 'mail_email_invalid',
'trace_status': 'cancel',
}, {
'status': 'canceled',
'records': self.test_records_failure_dupe,
'fields_values': {
'schedule_date': date_reference,
'state_msg': 'Email canceled',
},
# wrong email -> trace set as ignored
'trace_email': self.test_records_failure_dupe.email_normalized,
'trace_failure_type': 'mail_dup',
'trace_status': 'cancel',
}, {
'status': 'canceled',
'records': (test_records_1_ko - self.test_records_failure_wrong - self.test_records_failure_dupe),
'fields_values': {
'schedule_date': date_reference,
'state_msg': 'Email canceled',
},
# no email -> trace set as ignored
'trace_failure_type': 'mail_email_missing',
'trace_status': 'cancel',
}],
act1,
)
# Child traces should have been generated for all traces of parent activity as activity_domain
# is taken into account at processing, not generation (see act2_2)
self.assertMarketAutoTraces(
[{
'status': 'scheduled',
'records': test_records_init,
'participants': campaign.participant_ids,
'fields_values': {
'schedule_date': False,
},
}],
act2_1,
)
self.assertMarketAutoTraces(
[{
'status': 'scheduled',
'records': test_records_init,
'participants': campaign.participant_ids,
'fields_values': {
'schedule_date': date_reference + relativedelta(days=1),
},
}],
act2_2,
)
# a cron.trigger has been created to execute activities 1 day after mailing is sent
# there should only be one since we have 9 activities with the same scheduled_date
self.assertEqual(1, len(captured_triggers.records))
captured_trigger = captured_triggers.records[0]
self.assertEqual(
self.env.ref('marketing_automation.ir_cron_campaign_execute_activities'),
captured_trigger.cron_id)
self.assertEqual(self.date_reference + relativedelta(days=1), captured_trigger.call_at)
# Processing does not change anything (not time yet)
with freeze_time(self.date_reference):
campaign.execute_activities()
self.assertEqual(set(act2_1.trace_ids.mapped('state')), set(['scheduled']))
self.assertEqual(set(act2_2.trace_ids.mapped('state')), set(['scheduled']))
# ACT1 FOLLOWUP: PROCESS SOME REPLIES (+1 H)
# ------------------------------------------------------------
date_reference_reply = date_reference + relativedelta(hours=1)
test_records_1_replied = test_records_1_ok[:2]
with freeze_time(date_reference_reply), \
self.capture_triggers('marketing_automation.ir_cron_campaign_execute_activities') as captured_triggers:
for record in test_records_1_replied:
self.gateway_mail_reply_wrecord(MAIL_TEMPLATE, record)
self.assertMarketAutoTraces(
[{
'status': 'processed',
'records': test_records_1_replied,
'trace_status': 'reply',
'fields_values': {
'schedule_date': date_reference,
},
}, {
'status': 'processed',
'records': test_records_1_ok - test_records_1_replied,
'trace_status': 'sent',
'fields_values': {
'schedule_date': date_reference,
},
}, {
'status': 'canceled',
'records': self.test_records_failure_wrong,
'fields_values': {
'schedule_date': date_reference,
'state_msg': 'Email canceled',
},
# wrong email -> trace set as ignored
'trace_email': self.test_records_failure_wrong.email_from,
'trace_failure_type': 'mail_email_invalid',
'trace_status': 'cancel',
}, {
'status': 'canceled',
'records': self.test_records_failure_dupe,
'fields_values': {
'schedule_date': date_reference,
'state_msg': 'Email canceled',
},
# wrong email -> trace set as ignored
'trace_email': self.test_records_failure_dupe.email_normalized,
'trace_failure_type': 'mail_dup',
'trace_status': 'cancel',
}, {
'status': 'canceled',
'records': (test_records_1_ko - self.test_records_failure_wrong - self.test_records_failure_dupe),
'fields_values': {
'schedule_date': date_reference,
'state_msg': 'Email canceled',
},
# no email -> trace set as ignored
'trace_failure_type': 'mail_email_missing',
'trace_status': 'cancel',
}],
act1,
)
# Replied records -> SMS scheduled
self.assertMarketAutoTraces(
[{
'status': 'scheduled',
'records': test_records_1_replied,
'fields_values': {
'schedule_date': date_reference_reply + relativedelta(hours=1),
},
}, {
'status': 'scheduled',
'records': test_records_init - test_records_1_replied,
'fields_values': {
'schedule_date': False,
},
}],
act2_1,
)
# Replied records -> mail_not_open canceled
self.assertMarketAutoTraces(
[{
'status': 'scheduled',
'records': test_records_init - test_records_1_replied,
'fields_values': {
'schedule_date': date_reference + relativedelta(days=1),
},
}, {
'status': 'canceled',
'records': test_records_1_replied,
'fields_values': {
'schedule_date': date_reference_reply,
},
}],
act2_2,
)
# a cron.trigger has been created after each separate reply exactly 1 hour after the reply
# to match the created marketing.trace (ACT2.1)
# (here we have 2 replies considered at the exact same time but real use cases will most
# likely not)
self.assertEqual(len(captured_triggers.records), 2)
for captured_trigger in captured_triggers.records:
self.assertEqual(
captured_trigger.cron_id,
self.env.ref('marketing_automation.ir_cron_campaign_execute_activities'))
self.assertEqual(captured_trigger.call_at, date_reference_reply + relativedelta(hours=1))
# ACT2_1: REPLIED GOT AN SMS (+2 H)
# ------------------------------------------------------------
date_reference_new = date_reference + relativedelta(hours=2)
with freeze_time(date_reference_new), \
self.mockSMSGateway(), \
self.capture_triggers('marketing_automation.ir_cron_campaign_execute_activities') as captured_triggers:
campaign.execute_activities()
self.assertMarketAutoTraces(
[{
'status': 'processed',
'records': test_records_1_replied,
'fields_values': {
'schedule_date': date_reference_reply + relativedelta(hours=1),
},
'trace_status': 'outgoing',
}, {
'status': 'scheduled',
'records': test_records_init - test_records_1_replied,
'fields_values': {
'schedule_date': False,
},
}],
act2_1,
)
self.assertMarketAutoTraces(
[{
'status': 'scheduled',
'records': test_records_1_replied,
'fields_values': {
'schedule_date': False,
},
}],
act3_1,
)
self.assertFalse(captured_triggers.records) # no trigger should be created
with freeze_time(date_reference_new), \
self.mockSMSGateway(), \
self.capture_triggers('marketing_automation.ir_cron_campaign_execute_activities') as captured_triggers:
self.env['sms.sms'].sudo()._process_queue()
self.assertMarketAutoTraces(
[{
'status': 'processed',
'records': test_records_1_replied,
'fields_values': {
'schedule_date': date_reference_reply + relativedelta(hours=1),
},
'trace_status': 'pending',
}, {
'status': 'scheduled',
'records': test_records_init - test_records_1_replied,
'fields_values': {
'schedule_date': False,
},
}],
act2_1,
)
self.assertFalse(captured_triggers.records) # no trigger should be created
# ACT2_1 FOLLOWUP: CLICK ON LINKS -> ACT3_1: CONFIRMATION SMS SENT
# ------------------------------------------------------------
self._clear_outgoing_sms()
# TDE CLEANME: improve those tools, but sms gateway resets finding existing
# sms, which is why we do in two steps
test_records_1_clicked = test_records_1_replied[0]
sms_sent = self._find_sms_sent(test_records_1_clicked.customer_id, test_records_1_clicked.phone_sanitized)
# mock SMS gateway as in the same transaction, next activity is processed
with freeze_time(date_reference_new), \
self.mockSMSGateway(), \
self.capture_triggers('marketing_automation.ir_cron_campaign_execute_activities') as captured_triggers:
self.gateway_sms_sent_click(sms_sent)
self.assertFalse(captured_triggers.records) # no trigger should be created
with freeze_time(date_reference_new), \
self.mockSMSGateway(), \
self.capture_triggers('marketing_automation.ir_cron_campaign_execute_activities') as captured_triggers:
self.env['sms.sms'].sudo()._process_queue()
self.assertFalse(captured_triggers.records) # no trigger should be created
# click triggers process_event and automatically launches act3_1 depending on sms_click
self.assertMarketAutoTraces(
[{
'status': 'processed',
'records': test_records_1_clicked,
'fields_values': {
'schedule_date': date_reference_new,
},
# mailing trace
'trace_content': f'Confirmation for {test_records_1_clicked.name}',
'trace_status': 'pending',
}, {
'status': 'scheduled',
'records': test_records_1_replied - test_records_1_clicked,
'fields_values': {
'schedule_date': False,
},
}],
act3_1,
)
# ACT2_2: PROCESS SERVER ACTION ON NOT-REPLIED (+1D 2H)
# ------------------------------------------------------------
date_reference_new = date_reference + relativedelta(days=1, hours=2)
self._clear_outgoing_sms()
with freeze_time(date_reference_new), \
mute_logger('odoo.addons.marketing_automation.models.marketing_activity'), \
self.capture_triggers('marketing_automation.ir_cron_campaign_execute_activities') as captured_triggers:
campaign.execute_activities()
self.assertMarketAutoTraces(
[{
'status': 'processed',
'records': test_records_1_ok - test_records_1_replied,
'fields_values': {
'schedule_date': date_reference_new,
},
}, {
'status': 'error',
'records': (self.test_records_failure_wrong + self.test_records_failure_dupe), # server action did crash, description is False (see muted logger)
'fields_values': {
'schedule_date': date_reference_new,
'state_msg_content': 'Exception in server action',
},
}, {
'status': 'rejected',
'records': (test_records_1_ko - self.test_records_failure_wrong - self.test_records_failure_dupe), # no email_from -> rejected due to domain filter
'fields_values': {
'schedule_date': date_reference + relativedelta(days=1),
},
}, {
'status': 'canceled',
'records': test_records_1_replied, # replied -> mail_not_open is canceled
'fields_values': {
'schedule_date': date_reference_reply,
},
}],
act2_2,
)
# check server action was actually processed
for record in test_records_1_ko | test_records_1_replied:
self.assertNotIn('Did not answer, sad campaign is sad', (record.description or ''))
for record in test_records_1_ok - test_records_1_replied:
self.assertIn('Did not answer, sad campaign is sad', (record.description or ''))
self.assertFalse(captured_triggers.records) # no trigger should be created