/** * 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; }
/** * Delete function addendum: update statement's count * * @see https://github.com/Project60/CiviBanking/issues/59 */ static function del($ba_id) { // get batch (statement) id $ba_bao = new CRM_Banking_BAO_BankTransaction(); $ba_bao->get('id', $ba_id); $batch_id = $ba_bao->tx_batch_id; // delete the transaction / payments $ba_bao->delete(); // if $batch exists, update count if (!empty($batch_id)) { $new_count_query = "SELECT COUNT(`id`) FROM `civicrm_bank_tx` WHERE `tx_batch_id`='{$batch_id}'"; CRM_Core_DAO::executeQuery("UPDATE `civicrm_bank_tx_batch` SET `tx_count` = ({$new_count_query}) WHERE `id`='{$batch_id}';"); } }
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; }
/** * this matcher does not really create suggestions, but rather enriches the parsed data */ public function analyse(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context) { $config = $this->_plugin_config; $data_parsed = $btx->getDataParsed(); // itreate trough all rules foreach ($this->_plugin_config->rules as $rule) { if (empty($rule->fields)) { $fields = array('purpose'); } else { $fields = $rule->fields; } // replace [[...]] style variables in the pattern $pattern = $rule->pattern; $variables = $this->getVariableList(); foreach ($variables as $variable) { if (preg_match("#\\[\\[{$variable}\\]\\]#", $pattern)) { $value = $this->getVariable($variable); $pattern = preg_replace("#\\[\\[{$variable}\\]\\]#", print_r($value, 1), $pattern); } } // appy rule to all the fields listed... foreach ($fields as $field) { if (isset($data_parsed[$field])) { $field_data = $data_parsed[$field]; $matches = array(); // match the pattern on the given field data $match_count = preg_match_all($pattern, $field_data, $matches); // and execute the actions for each match... for ($i = 0; $i < $match_count; $i++) { $this->processMatch($matches, $i, $data_parsed, $rule); } } } } // save changes and that's it $btx->setDataParsed($data_parsed); }
/** * Gather all information on the transaction / payment * * @return an array containing all values, keys prefixed with 'tx_' */ public function getTxData($tx_id) { $result = array(); $tx = array(); $tx_bao = new CRM_Banking_BAO_BankTransaction(); $tx_bao->get('id', $tx_id); CRM_Core_DAO::storeValues($tx_bao, $tx); // add all basic fields foreach ($tx as $key => $value) { $result['tx_' . $key] = $value; } // resolve status IDs $result['tx_status'] = CRM_Core_OptionGroup::getValue('civicrm_banking.bank_tx_status', $result['tx_status_id'], 'id', 'String', 'name'); $result['tx_status_name'] = CRM_Core_OptionGroup::getValue('civicrm_banking.bank_tx_status', $result['tx_status_id'], 'id', 'String', 'label'); // add all data_parsed $data_parsed = $tx_bao->getDataParsed(); foreach ($data_parsed as $key => $value) { $result['data_' . $key] = $value; } unset($result['tx_data_parsed']); unset($result['tx_suggestions']); // add execution info $suggestion_objects = $tx_bao->getSuggestionList(); foreach ($suggestion_objects as $suggestion) { if ($suggestion->isExecuted()) { $result['exec_date'] = $suggestion->isExecuted(); $result['exec_executed_by'] = $suggestion->getParameter('executed_by'); $result['exec_automatically'] = $suggestion->getParameter('executed_automatically'); // find contribtion IDs $contribution_ids = array(); $suggestion_contribution_id = $suggestion->getParameter('contribution_id'); if (!empty($suggestion_contribution_id)) { if ((int) $suggestion_contribution_id) { $contribution_ids[] = (int) $suggestion_contribution_id; } } $suggestion_contribution_ids = $suggestion->getParameter('contribution_ids'); if (!empty($suggestion_contribution_ids)) { foreach ($suggestion_contribution_ids as $id) { $id = (int) $id; if ($id) { $contribution_ids[] = $id; } } } $result['exec_contribution_count'] = count($contribution_ids); $result['exec_contribution_list'] = implode(',', $contribution_ids); // also, add individual contribution data $counter = 1; $total_sum = 0.0; $total_currency = ''; $total_non_deductible = 0.0; foreach ($contribution_ids as $contribution_id) { $contribution = civicrm_api('Contribution', 'getsingle', array('id' => $contribution_id, 'version' => 3)); if (!empty($contribtion['is_error'])) { error_log("org.project60.banking.exporter.csv: error while reading contribution [{$contribution_id}]: " . $contribution['error_message']); } else { $prefix = 'exec_contribution' . ($counter > 1 ? "_{$counter}_" : '_'); foreach ($contribution as $key => $value) { $result[$prefix . $key] = $value; } if (!empty($contribution['total_amount'])) { $total_sum += $contribution['total_amount']; } if (!empty($contribution['non_deductible_amount'])) { $total_non_deductible += $contribution['non_deductible_amount']; } if (!empty($contribution['currency'])) { if (empty($total_currency)) { $total_currency = $contribution['currency']; } elseif ($total_currency != $contribution['currency']) { $total_currency = 'MIX'; } } } $counter++; } $result['exec_total_amount'] = $total_sum; $result['exec_total_currency'] = $total_currency; $result['exec_total_non_deductible'] = $total_non_deductible; break; } } return $result; }
/** * check if this ignore pattern applies to this btx */ private function matches_pattern($ignore_record, CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context) { // collect all the fields $fields = array(); if (isset($ignore_record->field)) { array_push($fields, $ignore_record->field); } if (isset($ignore_record->fields)) { $fields = array_merge($fields, $ignore_record->fields); } // extract the values $values = array(); foreach ($fields as $field) { if (isset($btx->{$field})) { array_push($values, $btx->{$field}); } else { $data = $btx->getDataParsed(); if (isset($data[$field])) { array_push($values, $data[$field]); } } } if (isset($ignore_record->regex)) { foreach ($values as $value) { if (preg_match($ignore_record->regex, $value)) { return true; } } } return false; }
/** * calculate the absolute probability based on the (possibly) relative value in the config */ private function get_probability($string_value, CRM_Banking_BAO_BankTransaction $btx) { if (substr($string_value, -1) === "%") { // if the value ends in '%' it's meant to be relative to the least probable suggestion $suggestion_list = $btx->getSuggestionList(); $least_probable = end($suggestion_list); if ($least_probable) { $least_probable_value = $least_probable->getProbability(); } else { $least_probable_value = 1; } return $least_probable_value * substr($string_value, 0, strlen($string_value) - 1) / 100.0; } else { // in the default case, we just assume it's an absolute value anyways... return $string_value; } }
/** * 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(); $penalty = $this->getPenalty(); // find potential contacts $contactID2probability = array(); if (empty($config->contact_id_list)) { $nameSearch = $context->findContacts($threshold, $data_parsed['name'], $config->lookup_contact_by_name); foreach ($nameSearch as $contact_id => $probability) { if ($probability - $penalty < $threshold) { continue; } $contactID2probability[$contact_id] = $probability; } } else { if (!empty($data_parsed[$config->contact_id_list])) { $id_list = explode(',', $data_parsed[$config->contact_id_list]); foreach ($id_list as $contact_id) { $contact_id = (int) $contact_id; if ($contact_id > 0) { $contactID2probability[$contact_id] = 1.0; } } } } // create suggestions if (!empty($config->search_terms)) { $query = $this->getPropagationSet($btx, $suggestion, '', $config->search_terms); } else { $query = array(); } if ($config->search_wo_contacts) { $suggestions = $this->createRecurringContributionSuggestions($query, 1.0, $btx, $context); } else { $suggestions = array(); foreach ($contactID2probability as $contact_id => $probability) { $query['contact_id'] = $contact_id; $new_suggestions = $this->createRecurringContributionSuggestions($query, $probability, $btx, $context); $suggestions = array_merge($suggestions, $new_suggestions); } } // apply penalties and threshold foreach ($suggestions as $suggestion) { $probability = $suggestion->getProbability(); $probability -= $penalty; if ($probability >= $threshold) { if ($penalty) { $suggestion->addEvidence($penalty, ts("A general penalty was applied.")); } $suggestion->setProbability($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) { // this section will be refactored to use different conditions, but for now, this is hardcoded $suggestion = new CRM_Banking_Matcher_Suggestion($this, $btx); $config = $this->_plugin_config; // amount range if (isset($config->amount)) { $camount = $config->amount; $low = $camount->low; $high = $camount->high; $factor = $camount->prob or 1; $amount = $btx->amount; if ($low == null || $amount >= $low) { if ($high == null || $amount <= $high) { $message = ts('the transaction amount is in the range [ '); if ($low) { $message .= number_format($low, 2); } $message .= ' - '; if ($high) { $message .= number_format($high, 2); } $message .= ' ]'; $suggestion->addEvidence($factor, $message); } } } // date range if (isset($config->value_date)) { $cvdate = $config->value_date; $early = $cvdate->early; $late = $cvdate->late; $factor = $cvdate->prob or 1; $value_date = strtotime($btx->value_date); if ($early != '' && $value_date >= strtotime($early)) { if ($late != '' && $value_date <= strtotime($late)) { $message = ts('the transaction value date is in the range [ '); if ($early) { $message .= $early; } $message .= ' - '; if ($late) { $message .= $late; } $message .= ' ]'; $suggestion->addEvidence($factor, $message); } } } // regex if (isset($config->purpose)) { $cpurp = $config->purpose; $regex = $cpurp->regex; $factor = $cpurp->prob or 1; $parsed = json_decode($btx->data_parsed, true); $purpose = $parsed['purpose']; if ($regex != '' && preg_match("/{$regex}/", $purpose)) { $message = sprintf(ts('the transaction purpose matches the expression "%s"'), htmlentities($regex)); $suggestion->addEvidence($factor, $message); } } if ($suggestion->getProbability() > 0) { $btx->addSuggestion($suggestion); } // close up return empty($this->_suggestions) ? null : $this->_suggestions; }
function _findBTX($status_id, $batch_id) { $btxs = array(); $btx_search = new CRM_Banking_BAO_BankTransaction(); $btx_search->limit(1999); if (!empty($status_id)) { $btx_search->status_id = (int) $status_id; } if (!empty($batch_id)) { $btx_search->tx_batch_id = (int) $batch_id; } $btx_search->find(); while ($btx_search->fetch()) { $btxs[] = array('id' => $btx_search->id, 'value_date' => $btx_search->value_date, 'sequence' => $btx_search->sequence, 'currency' => $btx_search->currency, 'amount' => $btx_search->amount, 'status_id' => $btx_search->status_id, 'data_parsed' => $btx_search->data_parsed, 'suggestions' => $btx_search->suggestions, 'ba_id' => $btx_search->ba_id, 'party_ba_id' => $btx_search->party_ba_id, 'tx_batch_id' => $btx_search->tx_batch_id); } if (count($btxs) >= 1999) { CRM_Core_Session::setStatus(sprintf(ts('Internal limit of 2000 transactions hit. Please use smaller statements.')), ts('List incomplete'), 'alert'); } return $btxs; }
/** * Bulk-run a set of <n> unprocessed items * * @param $max_count the maximal amount of bank transactions to process * * @return the actual amount of bank transactions prcoessed */ public function bulkRun($max_count) { $unprocessed_ids = CRM_Banking_BAO_BankTransaction::findUnprocessedIDs($max_count); foreach ($unprocessed_ids as $unprocessed_id) { $this->match($unprocessed_id); } return count($unprocessed_ids); }
/** * Will trigger the execution of the given suggestion (identified by its hash) */ function execute_suggestion($suggestion_hash, $parameters, $btx_bao, $choices) { // load BTX object if not provided if (!$btx_bao) { $btx_bao = new CRM_Banking_BAO_BankTransaction(); $btx_bao->get('id', $parameters['execute']); } $suggestion = $btx_bao->getSuggestionByHash($suggestion_hash); if ($suggestion) { // update the parameters $suggestion->update_parameters($parameters); $btx_bao->saveSuggestions(); $suggestion->execute($btx_bao); // create a notification bubble for the user $text = $suggestion->visualize_execution($btx_bao); if ($btx_bao->status_id == $choices['processed']['id']) { CRM_Core_Session::setStatus(ts("The transaction was booked.") . "<br/>" . $text, ts("Transaction closed"), 'info'); } elseif ($btx_bao->status_id == $choices['ignored']['id']) { CRM_Core_Session::setStatus(ts("The transaction was ignored.") . "<br/>" . $text, ts("Transaction closed"), 'info'); } else { CRM_Core_Session::setStatus(ts("The transaction could not be closed."), ts("Error"), 'alert'); } } else { CRM_Core_Session::setStatus(ts("Selected suggestions disappeared. Suggestion NOT executed!"), ts("Internal Error"), 'error'); } }
/** * 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(); $probability = 1.0 - $this->getPenalty($btx); $cancellation_mode = (bool) $config->cancellation_enabled && $btx->amount < 0; // look for the 'sepa_mandate' key if (empty($data_parsed['sepa_mandate'])) { return null; } // now load the mandate $mandate_reference = $data_parsed['sepa_mandate']; $mandate = civicrm_api('SepaMandate', 'getsingle', array('version' => 3, 'reference' => $mandate_reference)); if (!empty($mandate['is_error'])) { CRM_Core_Session::setStatus(sprintf(ts("Couldn't load SEPA mandate for reference %s"), $mandate_reference), ts('Error'), 'error'); return null; } // find the contribution if ($mandate['type'] == 'OOFF' && $mandate['entity_table'] == 'civicrm_contribution') { $contribution_id = $mandate['entity_id']; } elseif ($mandate['entity_table'] == 'civicrm_contribution_recur') { $contribution_recur_id = $mandate['entity_id']; $value_date = strtotime($btx->value_date); if ($cancellation_mode) { $earliest_date = date('Ymdhis', strtotime($config->cancellation_date_minimum, $value_date)); $latest_date = date('Ymdhis', strtotime($config->cancellation_date_maximum, $value_date)); } else { $earliest_date = date('Ymdhis', strtotime($config->received_date_minimum, $value_date)); $latest_date = date('Ymdhis', strtotime($config->received_date_maximum, $value_date)); } $contribution_id = 0; $find_contribution_query = "\n SELECT id\n FROM civicrm_contribution\n WHERE contribution_recur_id={$contribution_recur_id}\n AND receive_date <= DATE('{$latest_date}')\n AND receive_date >= DATE('{$earliest_date}');"; $found_contribution = CRM_Core_DAO::executeQuery($find_contribution_query); while ($found_contribution->fetch()) { if (!$contribution_id) { $contribution_id = $found_contribution->id; } else { // this is the second contribution found! CRM_Core_Session::setStatus(ts("There was more than one matching contribution found! Try to configure the plugin with a smaller search time span."), ts('Error'), 'error'); return null; } } if (!$contribution_id) { // no contribution found CRM_Core_Session::setStatus(ts("There was no matching contribution! Try to configure the plugin with a larger search time span."), ts('Error'), 'error'); return null; } } else { error_log("org.project60.sepa: matcher_sepa: Bad mandate type."); return null; } // now, let's have a look at this contribution and its contact... $contribution = civicrm_api('Contribution', 'getsingle', array('id' => $contribution_id, 'version' => 3)); if (!empty($contribution['is_error'])) { CRM_Core_Session::setStatus(ts("The contribution connected to this mandate could not be read."), ts('Error'), 'error'); return null; } $contact = civicrm_api('Contact', 'getsingle', array('id' => $contribution['contact_id'], 'version' => 3)); if (!empty($contact['is_error'])) { CRM_Core_Session::setStatus(ts("The contact connected to this mandate could not be read."), ts('Error'), 'error'); return null; } // now: create a suggestion $suggestion = new CRM_Banking_Matcher_Suggestion($this, $btx); $suggestion->setParameter('contribution_id', $contribution_id); $suggestion->setParameter('contact_id', $contribution['contact_id']); $suggestion->setParameter('mandate_id', $mandate['id']); $suggestion->setParameter('mandate_reference', $mandate_reference); if (!$cancellation_mode) { // STANDARD SUGGESTION: $suggestion->setTitle(ts("SEPA SDD Transaction")); // add penalties for deviations in amount,status,deleted contact if ($btx->amount != $contribution['total_amount']) { $suggestion->addEvidence($config->deviation_penalty, ts("The contribution does not feature the expected amount.")); $probability -= $config->deviation_penalty; } $status_inprogress = banking_helper_optionvalue_by_groupname_and_name('contribution_status', 'In Progress'); if ($contribution['contribution_status_id'] != $status_inprogress) { $suggestion->addEvidence($config->deviation_penalty, ts("The contribution does not have the expected status 'in Progress'.")); $probability -= $config->deviation_penalty; } if (!empty($contact['contact_is_deleted'])) { $suggestion->addEvidence($config->deviation_penalty, ts("The contact this mandate belongs to has been deleted.")); $probability -= $config->deviation_penalty; } } else { // CANCELLATION SUGGESTION: $suggestion->setTitle(ts("Cancel SEPA SDD Transaction")); $suggestion->setParameter('cancellation_mode', $cancellation_mode); // calculate penalties (based on CRM_Banking_PluginImpl_Matcher_ExistingContribution::rateContribution) $contribution_amount = $contribution['total_amount']; $target_amount = -$context->btx->amount; $amount_range_rel = $contribution_amount * ($config->cancellation_amount_relative_maximum - $config->cancellation_amount_relative_minimum); $amount_range_abs = $config->cancellation_amount_absolute_maximum - $config->cancellation_amount_absolute_minimum; $amount_range = max($amount_range_rel, $amount_range_abs); $amount_delta = $contribution_amount - $target_amount; // check for amount limits if ($amount_range) { $penalty = $config->cancellation_amount_penalty * (abs($amount_delta) / $amount_range); if ($penalty > $config->cancellation_penalty_threshold) { $suggestion->addEvidence($config->cancellation_amount_penalty, ts("The cancellation fee, i.e. the deviation from the original amount, is not in the specified range.")); $probability -= $penalty; } } // add general cancellation penalty, if set $probability -= (double) $config->cancellation_general_penalty; // generate cancellation extra parameters 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} - {$contribution_amount}"); 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', number_format($meval->evaluate($config->cancellation_cancel_fee_default), 2)); } catch (Exception $e) { error_log("org.project60.banking.matcher.existing: Couldn't calculate cancellation_fee. Error was: {$e}"); } } } // store it $suggestion->setProbability($probability); $btx->addSuggestion($suggestion); return $this->_suggestions; }