From 14d24938bcd8f6167a7c07ec99582a862c623145 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Sun, 30 Aug 2015 14:53:54 -0700 Subject: [PATCH] CRM-13422 - Add AbstractMappingTest. Fix soft-credit targeting. --- CRM/Contribute/ActionMapping/ByType.php | 1 + .../Contribute/ActionMapping/ByTypeTest.php | 185 +++++++++++++ .../ActionSchedule/AbstractMappingTest.php | 260 ++++++++++++++++++ 3 files changed, 446 insertions(+) create mode 100644 tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php create mode 100644 tests/phpunit/Civi/ActionSchedule/AbstractMappingTest.php diff --git a/CRM/Contribute/ActionMapping/ByType.php b/CRM/Contribute/ActionMapping/ByType.php index d977ae6a47..493c6fb8c7 100644 --- a/CRM/Contribute/ActionMapping/ByType.php +++ b/CRM/Contribute/ActionMapping/ByType.php @@ -229,6 +229,7 @@ class CRM_Contribute_ActionMapping_ByType implements \Civi\ActionSchedule\Mappin if ($schedule->recipient_listing && $schedule->limit_to) { switch ($schedule->recipient) { case 'soft_credit_type': + $query['casContactIdField'] = 'soft.contact_id'; $query->join('soft', 'INNER JOIN civicrm_contribution_soft soft ON soft.contribution_id = e.id') ->where("soft.soft_credit_type_id IN (#recipList)") ->param('recipList', \CRM_Utils_Array::explodePadded($schedule->recipient_listing)); diff --git a/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php new file mode 100644 index 0000000000..860107382d --- /dev/null +++ b/tests/phpunit/CRM/Contribute/ActionMapping/ByTypeTest.php @@ -0,0 +1,185 @@ + '2015-02-01 00:00:00', + 'to' => array('alice@example.org'), + 'subject' => '/Hello, Alice.*via subject/', + ), + ), + ); + + $cs[] = array( + '2015-02-01 00:00:00', + 'addAliceDues addBobDonation scheduleForAny startOnTime useHelloFirstName', + array( + array( + 'time' => '2015-02-01 00:00:00', + 'to' => array('alice@example.org'), + 'subject' => '/Hello, Alice.*via subject/', + ), + array( + 'time' => '2015-02-01 00:00:00', + 'to' => array('bob@example.org'), + 'subject' => '/Hello, Bob.*via subject/', + ), + ), + ); + + $cs[] = array( + '2015-02-02 00:00:00', + 'addAliceDues addBobDonation scheduleForDonation startWeekBefore repeatTwoWeeksAfter useHelloFirstName', + array( + array( + 'time' => '2015-01-26 00:00:00', + 'to' => array('bob@example.org'), + 'subject' => '/Hello, Bob.*via subject/', + ), + array( + 'time' => '2015-02-02 00:00:00', + 'to' => array('bob@example.org'), + 'subject' => '/Hello, Bob.*via subject/', + ), + array( + 'time' => '2015-02-09 00:00:00', + 'to' => array('bob@example.org'), + 'subject' => '/Hello, Bob.*via subject/', + ), + array( + 'time' => '2015-02-16 00:00:00', + 'to' => array('bob@example.org'), + 'subject' => '/Hello, Bob.*via subject/', + ), + ), + ); + + $cs[] = array( + '2015-02-03 00:00:00', + 'addAliceDues addBobDonation scheduleForSoftCreditor startWeekAfter useHelloFirstName', + array( + array( + 'time' => '2015-02-10 00:00:00', + 'to' => array('carol@example.org'), + 'subject' => '/Hello, Carol.*via subject/', + ), + ), + ); + + return $cs; + } + + public function addAliceDues() { + $this->callAPISuccess('Contribution', 'create', array( + 'contact_id' => $this->contacts['alice']['id'], + 'receive_date' => date('Ymd', strtotime($this->targetDate)), + 'total_amount' => '100', + 'financial_type_id' => 1, + 'non_deductible_amount' => '10', + 'fee_amount' => '5', + 'net_amount' => '95', + 'source' => 'SSF', + 'contribution_status_id' => 1, + 'soft_credit' => array( + '1' => array( + 'contact_id' => $this->contacts['carol']['id'], + 'amount' => 50, + 'soft_credit_type_id' => 3, + ), + ), + )); + } + + public function addBobDonation() { + $this->callAPISuccess('Contribution', 'create', array( + 'contact_id' => $this->contacts['bob']['id'], + 'receive_date' => date('Ymd', strtotime($this->targetDate)), + 'total_amount' => '150', + 'financial_type_id' => 2, + 'non_deductible_amount' => '10', + 'fee_amount' => '5', + 'net_amount' => '145', + 'source' => 'SSF', + 'contribution_status_id' => 2, + )); + } + + public function scheduleForDues() { + $this->schedule->mapping_id = CRM_Contribute_ActionMapping_ByType::MAPPING_ID; + $this->schedule->start_action_date = 'receive_date'; + $this->schedule->entity_value = CRM_Utils_Array::implodePadded(array(1)); + $this->schedule->entity_status = CRM_Utils_Array::implodePadded(array(1)); + } + + public function scheduleForDonation() { + $this->schedule->mapping_id = CRM_Contribute_ActionMapping_ByType::MAPPING_ID; + $this->schedule->start_action_date = 'receive_date'; + $this->schedule->entity_value = CRM_Utils_Array::implodePadded(array(2)); + $this->schedule->entity_status = CRM_Utils_Array::implodePadded(NULL); + } + + public function scheduleForAny() { + $this->schedule->mapping_id = CRM_Contribute_ActionMapping_ByType::MAPPING_ID; + $this->schedule->start_action_date = 'receive_date'; + $this->schedule->entity_value = CRM_Utils_Array::implodePadded(NULL); + $this->schedule->entity_status = CRM_Utils_Array::implodePadded(NULL); + } + + public function scheduleForSoftCreditor() { + $this->schedule->mapping_id = CRM_Contribute_ActionMapping_ByType::MAPPING_ID; + $this->schedule->start_action_date = 'receive_date'; + $this->schedule->entity_value = CRM_Utils_Array::implodePadded(NULL); + $this->schedule->entity_status = CRM_Utils_Array::implodePadded(NULL); + $this->schedule->limit_to = 1; + $this->schedule->recipient = 'soft_credit_type'; + $this->schedule->recipient_listing = CRM_Utils_Array::implodePadded(array(3)); + } + +} diff --git a/tests/phpunit/Civi/ActionSchedule/AbstractMappingTest.php b/tests/phpunit/Civi/ActionSchedule/AbstractMappingTest.php new file mode 100644 index 0000000000..2f7774cba9 --- /dev/null +++ b/tests/phpunit/Civi/ActionSchedule/AbstractMappingTest.php @@ -0,0 +1,260 @@ +targetDate`) + * and should relate to `$this->matchedContact`. Additionally, the + * setup-helper should created spurious contacts which are almost + * (but not quite) matched to the schedule rules. + * - Implement at least one schedule-helper which configures `$this->scheduled` + * to use the preferred action mapping. It may define various + * filters, such value-filters, status-filters, or recipient-filters. + * - Implement `createTestCases()` which defines various + * permutations of tests to run. + * + * For an example, see CRM_Contribute_ActionMapping_ByTypeTest. + */ +abstract class AbstractMappingTest extends \CiviUnitTestCase { + + /** + * @var \CRM_Core_DAO_ActionSchedule + */ + public $schedule; + + /** + * The date which should be stored on the matching record in the DB. + * + * @var string + */ + public $targetDate; + + /** + * Example contact records. + * + * @var array + */ + public $contacts; + + /** + * The schedule for invoking cron. + * + * @var array + * - start: string + * - end: string + * - interval: int, seconds + */ + public $cronSchedule; + + /** + * When comparing timestamps, treat them as the same if they + * occur within a certain distance of each other. + * + * @var int seconds + */ + public $dateTolerance = 120; + + /** + * @var \CiviMailUtils + */ + public $mut; + + /** + * Generate a list of test cases, where each is a distinct combination of + * data, schedule-rules, and schedule results. + * + * @return array + * - targetDate: string; eg "2015-02-01 00:00:01" + * - setupFuncs: string, space-separated list of setup functions + * - messages: array; each item is a message that's expected to be sent + * each message may include keys: + * - time: approximate time (give or take a few seconds) + * - subject: regex + * - message: regex + */ + public abstract function createTestCases(); + + // ---------------------------------------- Setup Helpers ---------------------------------------- + + /** + * Send first message on the designated date. + */ + public function startOnTime() { + $this->schedule->start_action_condition = 'before'; + $this->schedule->start_action_offset = '0'; + $this->schedule->start_action_unit = 'day'; + } + + /** + * Send first message one week before designated date. + */ + public function startWeekBefore() { + $this->schedule->start_action_condition = 'before'; + $this->schedule->start_action_offset = '7'; + $this->schedule->start_action_unit = 'day'; + } + + /** + * Send first message one week after designated date. + */ + public function startWeekAfter() { + $this->schedule->start_action_condition = 'after'; + $this->schedule->start_action_offset = '7'; + $this->schedule->start_action_unit = 'day'; + } + + /** + * Send repeated messages until two weeks after designated date. + */ + public function repeatTwoWeeksAfter() { + $this->schedule->is_repeat = 1; + $this->schedule->repetition_frequency_interval = '7'; + $this->schedule->repetition_frequency_unit = 'day'; + + $this->schedule->end_action = 'after'; + $this->schedule->end_date = $this->schedule->start_action_date; + $this->schedule->end_frequency_interval = '14'; + $this->schedule->end_frequency_unit = 'day'; + } + + /** + * Compose a "Hello" email which includes the recipient's first name. + */ + public function useHelloFirstName() { + $this->schedule->subject = 'Hello, {contact.first_name}. (via subject)'; + $this->schedule->body_html = '

Hello, {contact.first_name}. (via body_html)

'; + $this->schedule->body_text = 'Hello, {contact.first_name}. (via body_text)'; + } + + // ---------------------------------------- Core test definitions ---------------------------------------- + + /** + * Setup an empty schedule and some contacts. + */ + protected function setUp() { + parent::setUp(); + $this->useTransaction(); + + require_once 'CiviTest/CiviMailUtils.php'; + $this->mut = new \CiviMailUtils($this, TRUE); + + $this->cronSchedule = array( + 'start' => '2015-01-20 00:00:00', + 'end' => '2015-03-01 00:00:00', + 'interval' => 24 * 60 * 60, // seconds + ); + + $this->schedule = new \CRM_Core_DAO_ActionSchedule(); + $this->schedule->title = $this->getName(TRUE); + $this->schedule->name = \CRM_Utils_String::munge($this->schedule->title); + $this->schedule->is_active = 1; + $this->schedule->group_id = NULL; + $this->schedule->recipient = NULL; + $this->schedule->recipient_listing = NULL; + $this->schedule->recipient_manual = NULL; + $this->schedule->absolute_date = NULL; + $this->schedule->msg_template_id = NULL; + $this->schedule->record_activity = NULL; + + $this->contacts['alice'] = $this->callAPISuccess('Contact', 'create', array( + 'contact_type' => 'Individual', + 'first_name' => 'Alice', + 'last_name' => 'Exemplar', + 'email' => 'alice@example.org', + )); + $this->contacts['bob'] = $this->callAPISuccess('Contact', 'create', array( + 'contact_type' => 'Individual', + 'first_name' => 'Bob', + 'last_name' => 'Exemplar', + 'email' => 'bob@example.org', + )); + $this->contacts['carol'] = $this->callAPISuccess('Contact', 'create', array( + 'contact_type' => 'Individual', + 'first_name' => 'Carol', + 'last_name' => 'Exemplar', + 'email' => 'carol@example.org', + )); + } + + /** + * Execute the default schedule, without any special recipient selections. + * + * @dataProvider createTestCases + */ + public function testDefault($targetDate, $setupFuncs, $expectMessages) { + $this->targetDate = $targetDate; + + foreach (explode(' ', $setupFuncs) as $setupFunc) { + $this->{$setupFunc}(); + } + $this->schedule->save(); + + $actualMessages = array(); + foreach ($this->cronTimes() as $time) { + \CRM_Utils_Time::setTime($time); + $this->callAPISuccess('job', 'send_reminder', array()); + foreach ($this->mut->getAllMessages('ezc') as $message) { + /** @var \ezcMail $message */ + $simpleMessage = array( + 'time' => $time, + 'to' => \CRM_Utils_Array::collect('email', $message->to), + 'subject' => $message->subject, + ); + sort($simpleMessage['to']); + $actualMessages[] = $simpleMessage; + $this->mut->clearMessages(); + } + } + + $errorText = "Incorrect messages: " . print_r(array( + 'actualMessages' => $actualMessages, + 'expectMessages' => $expectMessages, + ), 1); + $this->assertEquals(count($expectMessages), count($actualMessages), $errorText); + usort($expectMessages, array(__CLASS__, 'compareSimpleMsgs')); + usort($actualMessages, array(__CLASS__, 'compareSimpleMsgs')); + foreach ($expectMessages as $offset => $expectMessage) { + $actualMessage = $actualMessages[$offset]; + $this->assertApproxEquals(strtotime($expectMessage['time']), strtotime($actualMessage['time']), $this->dateTolerance, $errorText); + if (isset($expectMessage['to'])) { + sort($expectMessage['to']); + $this->assertEquals($expectMessage['to'], $actualMessage['to'], $errorText); + } + if (isset($expectMessage['subject'])) { + $this->assertRegExp($expectMessage['subject'], $actualMessage['subject'], $errorText); + } + } + } + + protected function cronTimes() { + $skew = 0; + $times = array(); + $end = strtotime($this->cronSchedule['end']); + for ($time = strtotime($this->cronSchedule['start']); $time < $end; $time += $this->cronSchedule['interval']) { + $times[] = date('Y-m-d H:i:s', $time + $skew); + //$skew++; + } + return $times; + } + + protected function compareSimpleMsgs($a, $b) { + if ($a['time'] != $b['time']) { + return ($a['time'] < $b['time']) ? 1 : -1; + } + if ($a['to'] != $b['to']) { + return ($a['to'] < $b['to']) ? 1 : -1; + } + if ($a['subject'] != $b['subject']) { + return ($a['subject'] < $b['subject']) ? 1 : -1; + } + } + +} -- 2.25.1