/** * Generate a set of suggestions for the given bank transaction * * @return array(match structures) */ public function match(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context) { $config = $this->_plugin_config; $threshold = $this->getThreshold(); $penalty = $this->getPenalty($btx); $data_parsed = $btx->getDataParsed(); // first see if all the required values are there if (!$this->requiredValuesPresent($btx)) { return null; } // then look up potential contacts $contacts_found = $context->findContacts($threshold, $data_parsed['name'], $config->lookup_contact_by_name); // finally generate suggestions foreach ($contacts_found as $contact_id => $contact_probability) { $suggestion = new CRM_Banking_Matcher_Suggestion($this, $btx); $suggestion->setTitle(ts("Create a new contribution")); $suggestion->setId("create-{$contact_id}"); $suggestion->setParameter('contact_id', $contact_id); // set probability manually, I think the automatic calculation provided by ->addEvidence might not be what we need here $contact_probability -= $penalty; if ($contact_probability >= $threshold) { $suggestion->setProbability($contact_probability); $btx->addSuggestion($suggestion); } } // that's it... return empty($this->_suggestions) ? null : $this->_suggestions; }
/** * Generate a set of suggestions for the given bank transaction * * @return array(match structures) */ public function match(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context) { $config = $this->_plugin_config; $threshold = $this->getThreshold(); $data_parsed = $btx->getDataParsed(); // find potential contacts $contacts_found = $context->findContacts($threshold, $data_parsed['name'], $config->lookup_contact_by_name); // with the identified contacts, look up matching memberships $memberships = $this->findMemberships($contacts_found, $btx, $context); // transform all memberships into suggestions foreach ($memberships as $membership) { $suggestion = new CRM_Banking_Matcher_Suggestion($this, $btx); if (isset($contact->general_options->suggestion_title)) { $suggestion->setTitle($contact->general_options->suggestion_title); } else { $suggestion->setTitle(ts("Record as Membership Fee")); } $suggestion->setId("existing-{$contribution_id}"); $suggestion->setParameter('membership_id', $membership['id']); $suggestion->setParameter('last_fee_id', $membership['last_fee_id']); $suggestion->setProbability($membership['probability']); $btx->addSuggestion($suggestion); } // that's it... return empty($this->_suggestions) ? null : $this->_suggestions; }
public function match(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context) { $config = $this->_plugin_config; // create 'manually processed' suggestion, if applicable if ($config->manual_enabled) { if ($config->manual_show_always || $this->has_other_suggestions($btx)) { $manually_processed = new CRM_Banking_Matcher_Suggestion($this, $btx); $manually_processed->setProbability($this->get_probability($config->manual_probability, $btx)); $manually_processed->setTitle($config->manual_title); $manually_processed->setId('manual'); // add related contacts $data_parsed = $btx->getDataParsed(); $contacts = $context->findContacts(0, $data_parsed['name'], $config->lookup_contact_by_name); $manually_processed->setParameter('contact_ids', implode(',', array_keys($contacts))); $manually_processed->setParameter('contact_ids2probablility', json_encode($contacts)); $btx->addSuggestion($manually_processed); } } // create 'not relevant' suggestion, if applicable if ($config->ignore_enabled) { if ($config->ignore_show_always || $this->has_other_suggestions($btx)) { $not_relevant = new CRM_Banking_Matcher_Suggestion($this, $btx); $not_relevant->addEvidence($this->get_probability($config->ignore_probability, $btx)); $not_relevant->setTitle($config->ignore_title); $not_relevant->setId('ignore'); $btx->addSuggestion($not_relevant); } } // that's it... return empty($this->_suggestions) ? null : $this->_suggestions; }
/** * create suggestions for matching recurring contributions */ function createRecurringContributionSuggestions($query, $probability, $btx, $context) { $config = $this->_plugin_config; $threshold = $this->getThreshold(); $data_parsed = $btx->getDataParsed(); $suggestions = array(); // don't waste your time contacts below the threshold... if ($probability < $threshold) { return $suggestions; } $rcur_result = civicrm_api3('ContributionRecur', 'get', $query); foreach ($rcur_result['values'] as $rcur_id => $rcur) { // find the next expected date for the recurring contribution $expected_date = self::getExpectedDate($rcur, $btx, $config->recurring_mode); if ($expected_date == NULL) { continue; } // create a suggestion $suggestion = new CRM_Banking_Matcher_Suggestion($this, $btx); $suggestion->setId("recurring-{$rcur_id}"); $suggestion->setParameter('recurring_contribution_id', $rcur_id); $suggestion->setParameter('contact_id', $rcur['contact_id']); $suggestion->setParameter('expected_date', date('Y-m-d', $expected_date)); $suggestion->setParameter('expected_amount', $rcur['amount']); if (!empty($config->suggestion_title)) { $suggestion->setTitle($config->suggestion_title); } if ($probability < 1.0) { $suggestion->addEvidence(1.0 - $probability, ts("The contact could not be uniquely identified.")); } // CHECK AMOUNT if ($config->amount_check) { // calculate the amount penalties (equivalent to CRM_Banking_PluginImpl_Matcher_ExistingContribution) $transaction_amount = $btx->amount; $expected_amount = $rcur['amount']; $amount_delta = $transaction_amount - $expected_amount; if ($transaction_amount < $expected_amount * $config->amount_relative_minimum && $amount_delta < $config->amount_absolute_minimum) { continue; } if ($transaction_amount > $expected_amount * $config->amount_relative_maximum && $amount_delta > $config->amount_absolute_maximum) { continue; } $amount_range_rel = $transaction_amount * ($config->amount_relative_maximum - $config->amount_relative_minimum); $amount_range_abs = $config->amount_absolute_maximum - $config->amount_absolute_minimum; $amount_range = max($amount_range_rel, $amount_range_abs); if ($amount_range) { $penalty = $config->amount_penalty * (abs($amount_delta) / $amount_range); if ($penalty) { $suggestion->addEvidence($penalty, ts("The amount of the transaction differs from the expected amount.")); $probability -= $penalty; } } } // CHECK CURRENCY if ($context->btx->currency != $rcur['currency']) { $suggestion->addEvidence($config->currency_penalty, ts("The currency of the transaction is not as expected.")); $probability -= $config->currency_penalty; } // CHECK EXPECTED DATE if ($config->received_date_check) { // use date only $transaction_date = strtotime(date('Y-m-d', strtotime($context->btx->value_date))); // only apply penalties, if the offset is outside the accepted range $date_offset = $transaction_date - $expected_date; if ($date_offset < strtotime($config->acceptable_date_offset_from, 0) || $date_offset > strtotime($config->acceptable_date_offset_to, 0)) { // check if the payment is completely out of bounds if ($date_offset < strtotime($config->date_offset_minimum, 0)) { continue; } if ($date_offset > strtotime($config->date_offset_maximum, 0)) { continue; } // calculate the date penalties $date_range = strtotime($config->date_offset_maximum) - strtotime($config->date_offset_minimum) - (strtotime($config->acceptable_date_offset_to) - strtotime($config->acceptable_date_offset_from)); if ($date_offset < 0) { $date_delta = abs($date_offset - strtotime($config->acceptable_date_offset_from, 0)); } else { $date_delta = abs($date_offset - strtotime($config->acceptable_date_offset_to, 0)); } if ($date_range) { $penalty = $config->date_penalty * ($date_delta / $date_range); if ($penalty) { $suggestion->addEvidence($penalty, ts("The date of the transaction deviates too much from the expected date.")); $probability -= $penalty; } } } } // CHECK FOR OTHER PAYMENTS if ($config->existing_check) { $other_contributions_id_list = array(); $expected_date_string = date('Y-m-d', $expected_date); if (empty($config->existing_status_list)) { $config->existing_status_list = array(1, 2); } $existing_status_list = implode(',', $config->existing_status_list); // determine date range // TODO: use date_offset_minimum/maximum if (preg_match("/[0-9]+%/", $config->existing_precision)) { $cycle_length_seconds = strtotime("+{$rcur['frequency_interval']} {$rcur['frequency_unit']}", 0); $date_range = (int) ((100.0 - (double) $config->existing_precision) / 100.0 * ((double) $cycle_length_seconds / (double) (60 * 60 * 24))); } else { $date_range = (int) $config->existing_precision; } $sql = "\n SELECT id AS contribution_id\n FROM civicrm_contribution \n WHERE contribution_recur_id = {$rcur_id}\n AND contribution_status_id IN ({$existing_status_list})\n AND (receive_date BETWEEN ('{$expected_date_string}' - INTERVAL {$date_range} DAY)\n AND ('{$expected_date_string}' + INTERVAL {$date_range} DAY) );"; $sql_query = CRM_Core_DAO::executeQuery($sql); while ($sql_query->fetch()) { $other_contributions_id_list[] = $sql_query->contribution_id; } if (!empty($other_contributions_id_list)) { $links = array(); if (count($other_contributions_id_list) == 1) { $message = ts("There is already another contribution recorded for this interval: "); } else { $message = ts("There are already multiple contributions recorded for this interval: "); } foreach ($other_contributions_id_list as $other_contributions_id) { $links[] = "<a href='" . CRM_Utils_System::url('civicrm/contact/view/contribution', "reset=1&id={$other_contributions_id}&cid={$rcur['contact_id']}&action=view") . "'>[{$other_contributions_id}]</a>"; } $message .= implode(', ', $links); $suggestion->addEvidence($config->existing_penalty, $message); $probability -= $config->existing_penalty; } } $suggestion->setProbability($probability); $suggestions[] = $suggestion; } return $suggestions; }
/** * Generate a set of suggestions for the given bank transaction * * @return array(match structures) */ public function match(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context) { $config = $this->_plugin_config; $threshold = $this->getThreshold(); $penalty = $this->getPenalty($btx); $data_parsed = $btx->getDataParsed(); // first see if all the required values are there if (!$this->requiredValuesPresent($btx)) { return null; } // resolve accepted states $accepted_status_ids = $this->getAcceptedContributionStatusIDs(); $contributions = array(); $contribution2contact = array(); $contribution2totalamount = array(); $contributions_identified = array(); // check if this is actually enabled if ($config->contribution_search) { // find contacts $contacts_found = $context->findContacts($threshold, $data_parsed['name'], $config->lookup_contact_by_name); // with the identified contacts, look up contributions foreach ($contacts_found as $contact_id => $contact_probabiliy) { if ($contact_probabiliy < $threshold) { continue; } $potential_contributions = $this->getPotentialContributionsForContact($contact_id, $context); foreach ($potential_contributions as $contribution) { // check for expected status if (!in_array($contribution['contribution_status_id'], $accepted_status_ids)) { continue; } $contribution_probability = $this->rateContribution($contribution, $context); // apply penalty $contribution_probability -= $penalty; if ($contribution_probability > $threshold) { $contributions[$contribution['id']] = $contribution_probability; $contribution2contact[$contribution['id']] = $contact_id; $contribution2totalamount[$contribution['id']] = $contribution['total_amount']; } } } } // add the contributions coming in from a list (if any) if (!empty($config->contribution_list)) { if (!empty($data_parsed[$config->contribution_list])) { $id_list = explode(',', $data_parsed[$config->contribution_list]); foreach ($id_list as $contribution_id_string) { $contribution_id = (int) $contribution_id_string; if ($contribution_id) { $contribution_bao = new CRM_Contribute_DAO_Contribution(); if ($contribution_bao->get('id', $contribution_id)) { $contribution = $contribution_bao->toArray(); // check for expected status if (!in_array($contribution['contribution_status_id'], $accepted_status_ids)) { continue; } $contribution_probability = $this->rateContribution($contribution, $context); // apply penalty $contribution_probability -= $penalty; if ($contribution_probability > $threshold) { $contributions[$contribution['id']] = $contribution_probability; $contribution2contact[$contribution['id']] = $contribution['contact_id']; $contribution2totalamount[$contribution['id']] = $contribution['total_amount']; $contacts_found[$contribution['contact_id']] = 1.0; $contributions_identified[] = $contribution['id']; } } } } } } // transform all of the contributions found into suggestions foreach ($contributions as $contribution_id => $contribution_probability) { $contact_id = $contribution2contact[$contribution_id]; $suggestion = new CRM_Banking_Matcher_Suggestion($this, $btx); if (!in_array($contribution_id, $contributions_identified)) { if ($contacts_found[$contact_id] >= 1.0) { $suggestion->addEvidence(1.0, ts("Contact was positively identified.")); } else { $suggestion->addEvidence($contacts_found[$contact_id], ts("Contact was likely identified.")); } } if ($contribution_probability >= 1.0) { $suggestion->setTitle(ts("Matching contribution found")); if ($config->mode != "cancellation") { $suggestion->addEvidence(1.0, ts("A pending contribution matching the transaction was found.")); } else { $suggestion->addEvidence(1.0, ts("This transaction is the <b>cancellation</b> of the below contribution.")); } } else { $suggestion->setTitle(ts("Possible matching contribution found")); if ($config->mode != "cancellation") { $suggestion->addEvidence($contacts_found[$contact_id], ts("A pending contribution partially matching the transaction was found.")); } else { $suggestion->addEvidence($contacts_found[$contact_id], ts("This transaction could be the <b>cancellation</b> of the below contribution.")); } } $suggestion->setId("existing-{$contribution_id}"); $suggestion->setParameter('contribution_id', $contribution_id); $suggestion->setParameter('contact_id', $contact_id); $suggestion->setParameter('mode', $config->mode); // generate cancellation extra parameters if ($config->mode == 'cancellation') { if ($config->cancellation_cancel_reason) { // determine the cancel reason if (empty($data_parsed[$config->cancellation_cancel_reason_source])) { $suggestion->setParameter('cancel_reason', $config->cancellation_cancel_reason_default); } else { $suggestion->setParameter('cancel_reason', $data_parsed[$config->cancellation_cancel_reason_source]); } } if ($config->cancellation_cancel_fee) { // calculate / determine the cancellation fee try { $meval = new EvalMath(); // first initialise variables 'difference' and 'source' $meval->evaluate("difference = -{$btx->amount} - {$contribution2totalamount[$contribution_id]}"); if (empty($config->cancellation_cancel_fee_source) || empty($data_parsed[$config->cancellation_cancel_fee_source])) { $meval->evaluate("source = 0.0"); } else { $meval->evaluate("source = {$data_parsed[$config->cancellation_cancel_fee_source]}"); } $suggestion->setParameter('cancel_fee', $meval->evaluate($config->cancellation_cancel_fee_default)); } catch (Exception $e) { error_log("org.project60.banking.matcher.existing: Couldn't calculate cancellation_fee. Error was: {$e}"); } } } // set probability manually, I think the automatic calculation provided by ->addEvidence might not be what we need here $suggestion->setProbability($contribution_probability * $contacts_found[$contact_id]); // update title if requested if (!empty($config->title)) { $suggestion->setTitle($config->title); } $btx->addSuggestion($suggestion); } // that's it... return empty($this->_suggestions) ? null : $this->_suggestions; }
public function match(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context) { // get list of existing batches (cache in context) $existing_batches = $context->getCachedEntry('banking.pluginimpl.matcher.batch'); if ($existing_batches == NULL) { $existing_batches = $this->generateBatchList(); $context->setCachedEntry('banking.pluginimpl.matcher.batch', $existing_batches); } // look for a matching batch $config = $this->_plugin_config; $booking_date = strtotime($btx->booking_date); $matching_batches = array(); foreach ($existing_batches as $batch) { $total_amount = $batch['total']; if (!empty($batch['export_date'])) { $submission_date = strtotime($batch['export_date']); } elseif ($batch['modified_date']) { $submission_date = strtotime($batch['modified_date']); } else { $submission_date = strtotime($batch['created_date']); } // check amount if (abs(1 - $total_amount / $btx->amount) > $config->total_amount_tolerance) { continue; } // check export_date_to_payment_min / max if ($booking_date < strtotime($config->export_date_to_payment_min, $submission_date)) { continue; } if ($booking_date > strtotime($config->export_date_to_payment_max, $submission_date)) { continue; } // batch is accepted -> calculate probability: // first factor: expected income time $time_penalty_total = strtotime('-' . $config->export_date_to_payment_tolerance, abs($booking_date - $submission_date)); $time_penalty = min(1.0, 1 - $time_penalty_total / (strtotime($config->export_date_to_payment_max) - strtotime($config->export_date_to_payment_min))); // second factor: equal amount $amount_penalty = 1.0 - abs(1 - $total_amount / $btx->amount) / $config->total_amount_tolerance; // third factor: statmentes pending $status_penalty = 1.0 - count($this->getNonPendingContributionIDs($batch['id'])) / $batch['item_count']; $matching_batches[$batch['id']] = $time_penalty * $amount_penalty * $status_penalty; } // for each matched batch, create a suggestion foreach ($matching_batches as $batch_id => $batch_probability) { $suggestion = new CRM_Banking_Matcher_Suggestion($this, $btx); $suggestion->setTitle(ts("Settles a contribution batch")); $suggestion->setParameter('batch_id', $batch_id); $suggestion->setId("batch-" . $batch_id); $suggestion->setProbability($batch_probability); $btx->addSuggestion($suggestion); } // that's it... return empty($this->_suggestions) ? null : $this->_suggestions; }