CORE-POS/IS4C

View on GitHub
fannie/modules/plugins2.0/MailChimpSync/MailChimpTask.php

Summary

Maintainability
D
3 days
Test Coverage
<?php
/*******************************************************************************

    Copyright 2013 Whole Foods Co-op

    This file is part of IT CORE.

    IT CORE is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    IT CORE is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    in the file license.txt along with IT CORE; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*********************************************************************************/

use DrewM\MailChimp\MailChimp as MailChimpV3;

include_once(dirname(__FILE__) . '/../../../config.php');
if (!class_exists('FannieAPI')) {
    include(dirname(__FILE__) . '/../../../classlib2.0/FannieAPI.php');
}

class MailChimpTask extends FannieTask
{
    private $requiredMergeFields = array();
    private $ownerField = 'OWNER NUMBER';

    protected function getSettings()
    {
        $FANNIE_PLUGIN_SETTINGS = $this->config->get('PLUGIN_SETTINGS');
        $APIKEY = $FANNIE_PLUGIN_SETTINGS['MailChimpApiKey'];
        $LISTID = $FANNIE_PLUGIN_SETTINGS['MailChimpListID'];
        if ($FANNIE_PLUGIN_SETTINGS['MailChimpMergeVarName']) {
            $this->ownerField =$FANNIE_PLUGIN_SETTINGS['MailChimpMergeVarName'];
        }

        return array($APIKEY, $LISTID);
    }

    protected function readMergeFields($chimp, $LISTID)
    {
        $vars = $chimp->get("lists/{$LISTID}/merge-fields");
        $field_id = false;
        foreach ($vars['merge_fields'] as $mf) {
            if ($mf['tag'] == 'CARDNO') {
                $field_id = $mf['merge_id'];
            }
            if ($mf['required']) {
                $this->requiredMergeFields[$mf['tag']] = $mf['default_value'];
            }
        }

        if ($field_id === false) {
            echo 'Adding member# field' . "\n";
            /** doesn't worK?
            $new = $chimp->post("lists/{$LISTID}/merge_fields", array(
                'tag' => 'CARDNO',
                'name' => 'Owner Number',
                'type' => 'number',
                'required' => false,
                'public' => false,
            ));
            $field_id = $new['merge_id'];
             */
        }

        /*
        if ($field_id === false) {
            $this->cronMsg('Error: could not locate / create owner number field!', FannieLogger::NOTICE);
            return false;
        }
         */

        return true;
    }

    public function run()
    {
        global $FANNIE_OP_DB, $FANNIE_PLUGIN_SETTINGS, $argv;
        $dbc = FannieDB::get($FANNIE_OP_DB);
        $this->custdata = new CustdataModel($dbc);
        $this->meminfo = new MeminfoModel($dbc);

        list($APIKEY, $LISTID) = $this->getSettings();
        if (empty($APIKEY) || empty($LISTID)) {
            $this->cronMsg('Missing API key or List ID', FannieLogger::NOTICE);
            return false;
        }

        if (!class_exists('Mailchimp')) {
            $this->cronMsg('MailChimp library is not installed', FannieLogger::NOTICE);
            return false;
        } elseif (!class_exists('DrewM\\MailChimp\\MailChimp')) {
            $this->cronMsg('MailChimp API v3 library is not installed', FannieLogger::NOTICE);
            return false;
        }
        $chimpEX = new MailChimpEx($APIKEY);
        $chimp3 = new MailChimpV3($APIKEY);

        if ($this->readMergeFields($chimp3, $LISTID) === false) {
            return false;
        } // end create owner number field if needed

        $statuses = array('subscribed', 'unsubscribed', 'cleaned');
        $cleans = array();
        $memlist = '';
        /**
          Examine all list members
        */
        foreach ($statuses as $status) {

            $this->cronMsg('==== Checking ' . $status . ' emails ====', FannieLogger::INFO);

            $full_list = $chimpEX->lists->export($LISTID, $status);
            $headers = array_shift($full_list);
            $columns = array();
            foreach ($headers as $index => $name) {
                $columns[strtoupper($name)] = $index;
            }
            $line_count = 1;
            foreach ($full_list as $record) {
                /**
                  Print progress meter in verbose mode
                */
                if (isset($argv[2]) && ($argv[2] == '-v' || $argv[2] == '--verbose')) {
                    printf("Processing %d/%d\r", $line_count, count($full_list));
                }
                $line_count++;
                list($card_no, $email, $fname, $lname, $changed) = $this->unpackRecord($record, $columns);

                /** MailChimp has a POS member number tag **/
                if ($card_no !== false && !empty($card_no)) {
                    switch ($status) {
                        /**
                          If subscribed list member has been tagged with a POS member number, compare
                          MailChimp fields to POS fields. If name disagrees, use POS value
                          for both. If email disagrees, use MailChimp value for both.
                        */
                        case 'subscribed':
                            $memlist = $this->isSubscribed($record, $columns, $chimp3, $LISTID, $memlist);
                            break;
                        /**
                          Just track the number to avoid re-adding them to the list
                        */
                        case 'unsubscribed':
                            $memlist = $this->isUnsubscribed($record, $columns, $memlist);
                            break;
                        /**
                          Cleaned in MailChimp means the address isn't deliverable
                          In this situation, remove the bad address from POS
                          and delete the address from MailChimp. The member can be
                          re-added when a correct email is entered into POS.
                        */
                        case 'cleaned':
                            $memlist = $this->isCleaned($record, $columns, $memlist);
                            $cleans[] = $record;
                            break;
                    }
                /**
                  If list member is not tagged with a POS member number, try to
                  locate them in POS by name and/or email address. If found,
                  tag them in MailChimp with the POS member number. This whole
                  situation only occurs if the initial list is imported without
                  member numbers.
                */
                } else {
                    switch ($status) {
                        // subscribed users can be updated easily
                        case 'subscribed':
                            $memlist = $this->unknownSubscribed($record, $columns, $chimp3, $LISTID, $memlist);
                            break;
                        /**
                          Unsubscribed are currently ignored. The can't be updated as is.
                          They could be deleted entirely via unsubscribe, resubscribed with
                          an owner number, and then unsubscribed again. That's not currently
                          implemented. It does check for the email address on the POS side
                          to prevent re-adding it.
                        */
                        case 'unsubscribed':
                            $memlist = $this->unknownUnsubscribed($record, $columns, $memlist);
                            break;
                        /**
                          Cleaned are bad addresses. Delete them from POS database
                          then from Mail Chimp.
                        */
                        case 'cleaned':
                            $memlist = $this->unknownClean($record, $columns, $memlist);
                            $cleans[] = $record;
                            break;
                    }
                }
            } // foreach list member record

        } // foreach list status

        $this->removeBounces($chimp3, $LISTID, $cleans, $columns);

        $this->addNew($chimp3, $LISTID, $dbc, $memlist);

        return true;
    }

    protected function removeBounces($chimp, $LISTID, $cleans, $columns)
    {
        $this->cronMsg(sprintf('Removing %d addresses with status "cleaned"', count($cleans)), FannieLogger::INFO);
        $delID = 0;
        $batch = $chimp->new_batch();
        foreach ($cleans as $record) {
            if (empty($record[$columns['EMAIL ADDRESS']])) {
                continue;
            }
            $email = $record[$columns['EMAIL ADDRESS']];
            $hash = $chimp->subscriberHash($email);
            $batch->delete("del{$delID}", "lists/{$LISTID}/members/{$hash}");
            $delID++;
            $this->cronMsg(sprintf('  => %s', $email), FannieLogger::INFO);
        }
        if ($delID) {
            $res = $batch->execute();
        }
    }

    protected function addNew($chimp, $LISTID, $dbc, $memlist)
    {
        /**
          Finally, find new members and add them to MailChimp
        */
        if ($memlist === '') {
            $memlist = '-1,';
        }
        $memlist = substr($memlist, 0, strlen($memlist)-1);
        $query = 'SELECT m.card_no,
                    m.email_1,
                    c.FirstName,
                    c.LastName
                  FROM meminfo AS m
                    INNER JOIN custdata AS c ON c.CardNo=m.card_no AND c.personNum=1
                  WHERE c.Type = \'PC\'
                    AND m.email_1 IS NOT NULL
                    AND m.email_1 <> \'\'
                    AND m.card_no NOT IN (' . $memlist . ')';
        $result = $dbc->query($query);
        $this->cronMsg(sprintf('Adding %d new members', $dbc->numRows($result)), FannieLogger::INFO);
        $batch = $chimp->new_batch();
        $addID = 0;
        while ($row = $dbc->fetch_row($result)) {
            if (!filter_var($row['email_1'], FILTER_VALIDATE_EMAIL)) {
                continue;
            }
            $this->cronMsg(sprintf(' => %s (%s)', $row['email_1'], $row['card_no']), FannieLogger::INFO);
            $req = array(
                'email_address' => $row['email_1'],
                'status' => 'subscribed',
                'merge_fields' => array(
                    'FNAME' => $row['FirstName'],
                    'LNAME' => $row['LastName'],
                ),
            );
            foreach ($this->requiredMergeFields as $tag => $default) {
                if (!isset($req['merge_fields'][$tag])) {
                    $req['merge_fields'][$tag] = $default;
                }
            }
            $batch->post("add{$addID}", "lists/{$LISTID}/members", $req);
            $addID++;
        }
        if ($addID) {
            $res = $batch->execute();
            /*
            for ($i=0; $i<25; $i++) {
                sleep(30);
                $status = $batch->check_status();
                if ($status['status'] == 'finished') {
                    $resultURL = $status['response_body_url'];
                    $this->cronMsg("Done. See results: " . $resultURL);
                    break;
                }
                $this->cronMsg("Add batch status: " . json_encode($status));
            }
             */
        }
    }

    protected function unpackRecord($record, $columns)
    {
        $card_no = isset($columns[$this->ownerField]) ? $record[$columns[$this->ownerField]] : false;
        $email = $record[$columns['EMAIL ADDRESS']];
        $fname = $record[$columns['FIRST NAME']];
        $lname = $record[$columns['LAST NAME']];
        $changed = isset($columns['LAST_CHANGED']) && isset($record[$columns['LAST_CHANGED']]) ? $record[$columns['LAST_CHANGED']] : 0;

        return array($card_no, $email, $fname, $lname, $changed);
    }

    /**
      Callback when list includes an subuscribed entry
      with a member number
    */
    protected function isSubscribed($record, $columns, $chimp, $LISTID, $memlist)
    {
        list($card_no, $email, $fname, $lname, $changed) = $this->unpackRecord($record, $columns);
        $memlist .= sprintf('%d,', $card_no);
        $this->custdata->reset();
        $this->custdata->CardNo($card_no);
        $this->custdata->personNum(1);
        $this->custdata->load();
        $update = array();
        $this->meminfo->reset();
        $this->meminfo->card_no($card_no);
        $this->meminfo->load();
        $email = strtolower($email);
        if (strtolower($this->meminfo->email_1()) != $email && (strtotime($changed) > strtotime($this->meminfo->modified()) || $this->meminfo->email_1() == '')) {
            $this->cronMsg(sprintf("MISMATCH: POS says %s, MailChimp says %s, Mailchimp is newer",
            $this->meminfo->email_1(), $email), FannieLogger::INFO);
            $this->meminfo->email_1($email);
            $this->meminfo->save();
        } elseif (strtolower($this->meminfo->email_1()) != $email) {
            $update['EMAIL'] = strtolower($this->meminfo->email_1());
            $this->cronMsg(sprintf("MISMATCH: POS says %s, MailChimp says %s, POS is newer",
                $this->meminfo->email_1(), $email), FannieLogger::ALERT);
        }
        if (strtoupper(trim($this->custdata->FirstName())) != strtoupper($fname)) {
            $this->cronMsg(sprintf("MISMATCH: POS says %s, MailChimp says %s",
                $this->custdata->FirstName(), $fname), FannieLogger::INFO);
            $update['FNAME'] = trim($this->custdata->FirstName());
        }
        if (strtoupper(trim($this->custdata->LastName())) != strtoupper($lname)) {
            $this->cronMsg(sprintf("MISMATCH: POS says %s, MailChimp says %s",
                $this->custdata->LastName(), $lname), FannieLogger::INFO);
            $update['LNAME'] = trim($this->custdata->LastName());
        }
        if (count($update) > 0) {
            $email_struct = array(
                'euid' => $record[$columns['EUID']],
                'leid' => $record[$columns['LEID']],
            );
            $this->cronMsg(sprintf("Updating name field(s) for member #%d", $card_no), FannieLogger::INFO);
            try {
                $hash = $chimp->subscriberHash($email);
                $req = array();
                if (isset($update['EMAIL'])) {
                    $req['email'] = $update['EMAIL'];
                }
                if (isset($update['FNAME']) || isset($update['LNAME'])) {
                    $req['merge_fields'] = array();
                    if (isset($update['FNAME'])) {
                        $req['merge_fields']['FNAME'] = $update['FNAME'];
                    }
                    if (isset($update['LNAME'])) {
                        $req['merge_fields']['LNAME'] = $update['LNAME'];
                    }
                }
                $chimp->patch("list/{$LISTID}/members/{$hash}", $req);
            } catch (Exception $ex) {
                echo $ex->getMessage();
            }
            sleep(1);
        }

        return $memlist;
    }

    /**
      Callback when list includes an unsubuscribed entry
      with a member number
    */
    protected function isUnsubscribed($record, $columns, $memlist)
    {
        list($card_no, $email, $fname, $lname, $changed) = $this->unpackRecord($record, $columns);
        $memlist .= sprintf('%d,', $card_no);

        return $memlist;
    }

    /**
      Callback when list includes a cleaned [invalid address] entry
      with a member number
    */
    protected function isCleaned($record, $columns, $memlist)
    {
        list($card_no, $email, $fname, $lname, $changed) = $this->unpackRecord($record, $columns);
        $this->meminfo->reset();
        $this->meminfo->card_no($card_no);
        $this->meminfo->email_1('');
        $this->meminfo->save();
        $this->cronMsg(sprintf('CLEANING Member %d, email %s', $card_no, $email), FannieLogger::INFO);

        return $memlist;
    }

    /**
      Callback when list includes a subscribed entry
      without a member number
    */
    protected function unknownSubscribed($record, $columns, $chimp, $LISTID, $memlist)
    {
        list($card_no, $email, $fname, $lname, $changed) = $this->unpackRecord($record, $columns);
        if ($card_no === false) {
            return $memlist;
        }
        $update = array();
        $this->meminfo->reset();
        $this->meminfo->email_1($email);
        $matches = $this->meminfo->find();
        if (count($matches) == 1) {
            $update['CARDNO'] = $matches[0]->card_no();
        } else {
            $this->custdata->reset();
            $this->custdata->FirstName($fname);
            $this->custdata->LastName($lname);
            $this->custdata->personNum(1);
            $this->custdata->Type('PC');
            $matches = $this->custdata->find();
            if (count($matches) == 1) {
                $update['CARDNO'] = $matches[0]->CardNo();
            }
        }

        if (isset($update['CARDNO'])) {
            $this->cronMsg("Assigning member # to account " . $email, FannieLogger::INFO);
            $hash = $chimp->subscriberHash($email);
            $chimp->patch("lists/{$LISTID}/members/{$hash}", array(
                'merge_fields' => array('CARDNO' => $update['CARDNO']),
            ));
            sleep(1);
            $memlist .= sprintf('%d,', $update['CARDNO']);
        }

        return $memlist;
    }

    /**
      Callback when list includes an unsubscribed entry
      without a member number
    */
    protected function unknownUnsubscribed($record, $columns, $memlist)
    {
        list($card_no, $email, $fname, $lname, $changed) = $this->unpackRecord($record, $columns);
        if ($memlist === false) {
            return $memlist;
        }
        $this->meminfo->reset();
        $this->cronMsg('Checking unsubscribed ' . $email, FannieLogger::INFO);
        $this->meminfo->email_1($email);
        $matches = $this->meminfo->find();
        foreach ($matches as $opted_out) {
            $memlist .= sprintf('%d,', $opted_out->card_no());
            $this->cronMsg('Excluding member ' . $opted_out->card_no(), FannieLogger::INFO);
        }

        return $memlist;
    }

    /**
      Callback when list includes a cleaned [invalid address] entry
      without a member number
    */
    protected function unknownClean($record, $columns, $memlist)
    {
        list($card_no, $email, $fname, $lname, $changed) = $this->unpackRecord($record, $columns);
        $this->meminfo->reset();
        $this->meminfo->email_1($email);
        foreach ($this->meminfo->find() as $bad_address) {
            $bad_address->email_1('');
            $bad_address->save();
            $this->cronMsg(sprintf('CLEANING untagged member %d, email %s', $bad_address->card_no(), $email), FannieLogger::INFO);
        }

        return $memlist;
    }

}