function adjustAmount($mandate_id) { // check if we are allowed to... if (CRM_Sepa_Logic_Settings::getSetting('allow_mandate_modification')) { $adjusted_amount = (double) $_REQUEST['adjust_amount']; if ($adjusted_amount > 0) { if (CRM_Sepa_BAO_SEPAMandate::adjustAmount($mandate_id, $adjusted_amount)) { CRM_Core_Session::setStatus(sprintf(ts("The amount of this mandate was modified. You should send out a new prenotification to the debtor.")), ts('Advice'), 'info'); } } else { CRM_Core_Session::setStatus(sprintf(ts("Invalid amount. Mandate not modified.")), ts('Error'), 'error'); } } else { CRM_Core_Session::setStatus(sprintf(ts("Modifying an existing mandate is currently not allowed. You can change this on the SEPA settings page.")), ts('Error'), 'error'); } }
public function buildQuickForm() { CRM_Utils_System::setTitle(ts('Sepa Direct Debit - Settings')); $customFields = CRM_Core_BAO_CustomField::getFields(); $cf = array(); foreach ($customFields as $k => $v) { $cf[$k] = $v['label']; } // add all form elements and validation rules foreach ($this->config_fields as $key => $value) { $elementName = $this->domainToString($value[0]); $elem = $this->addElement('text', $elementName, $value[1], isset($value[2]) ? $value[2] : array()); if (!in_array($elementName, array('cycledays', 'custom_txmsg'))) { // integer only rules, except for cycledays (list) $this->addRule($this->domainToString($value[0]), sprintf(ts("Please enter the %s as number (integers only)."), $value[1]), 'positiveInteger'); $this->addRule($this->domainToString($value[0]), sprintf(ts("Please enter the %s as number (integers only)."), $value[1]), 'required'); } } // country drop down field $config = CRM_Core_Config::singleton(); $i18n = CRM_Core_I18n::singleton(); $climit = array(); $cnames = array(); $ciso = array(); $filtered = array(); $climit = $config->countryLimit(); CRM_Core_PseudoConstant::populate($cnames, 'CRM_Core_DAO_Country', TRUE, 'name', 'is_active'); CRM_Core_PseudoConstant::populate($ciso, 'CRM_Core_DAO_Country', TRUE, 'iso_code'); foreach ($ciso as $key => $value) { foreach ($climit as $active_country) { if ($active_country == $value) { $filtered[$key] = $cnames[$key]; } } } $i18n->localizeArray($filtered, array('context' => 'country')); asort($filtered); // do not use array_merge() because it discards the original indizes $country_ids = array('' => ts('- select -')) + $filtered; $exw = CRM_Core_BAO_Setting::getItem('SEPA Direct Debit Preferences', 'exclude_weekends'); if ($exw) { $exw = array('checked' => 'checked'); } else { $exw = array(); } // add creditor form elements $this->addElement('text', 'addcreditor_creditor_id', ts("Creditor Contact")); $this->addElement('text', 'addcreditor_name', ts("Name")); $this->addElement('text', 'addcreditor_id', ts("Identifier")); $this->addElement('text', 'addcreditor_address', ts("Address")); $this->addElement('select', 'addcreditor_country_id', ts("Country"), $country_ids); $this->addElement('text', 'addcreditor_bic', ts("BIC")); $this->addElement('text', 'addcreditor_iban', ts("IBAN")); $this->addElement('select', 'addcreditor_pain_version', ts("PAIN Version"), array('' => ts('- select -')) + CRM_Core_OptionGroup::values('sepa_file_format')); $this->addElement('checkbox', 'is_test_creditor', ts("Is a Test Creditor"), "", array('value' => '0')); $this->addElement('checkbox', 'exclude_weekends', ts("Exclude Weekends"), "", $exw); $this->addElement('hidden', 'edit_creditor_id', '', array('id' => 'edit_creditor_id')); $this->addElement('hidden', 'add_creditor_id', '', array('id' => 'add_creditor_id')); // add custom form elements and validation rules $index = 0; foreach ($this->custom_fields as $key => $value) { $this->addElement('text', $this->domainToString($value[0]), $value[1], array('placeholder' => CRM_Core_BAO_Setting::getItem('SEPA Direct Debit Preferences', $this->domainToString($this->config_fields[$index][0])))); $elementName = $this->domainToString($value[0]); if (!in_array($elementName, array('custom_cycledays', 'custom_txmsg'))) { // integer only rules, except for cycledays (list) $this->addRule($elementName, sprintf(ts("Please enter the %s as number (integers only)."), $value[1]), 'positiveInteger'); } $index++; } // register and add extra validation rules $this->registerRule('sepa_cycle_day_list', 'callback', 'sepa_cycle_day_list', 'CRM_Sepa_Logic_Settings'); $this->addRule('cycledays', ts('Please give a comma separated list of valid days.'), 'sepa_cycle_day_list'); $this->addRule('custom_cycledays', ts('Please give a comma separated list of valid days.'), 'sepa_cycle_day_list'); // get creditor list $creditors_default_list = array(); $creditor_query = civicrm_api('SepaCreditor', 'get', array('version' => 3, 'option.limit' => 99999)); if (!empty($creditor_query['is_error'])) { return civicrm_api3_create_error("Cannot get creditor list: " . $creditor_query['error_message']); } else { $creditors = array(); foreach ($creditor_query['values'] as $creditor) { $creditors[] = $creditor; $creditors_default_list[$creditor['id']] = $creditor['name']; } } $this->assign('creditors', $creditors); $default_creditors = $this->addElement('select', 'batching_default_creditor', ts("Default Creditor"), array('' => ts('- select -')) + $creditors_default_list); $default_creditors->setSelected(CRM_Sepa_Logic_Settings::getSetting('batching.default.creditor')); // add general config options $amm_options = CRM_Sepa_Logic_Settings::getSetting('allow_mandate_modification') ? array('checked' => 'checked') : array(); $this->addElement('checkbox', 'allow_mandate_modification', ts("Mandate Modifications"), NULL, $amm_options); parent::buildQuickForm(); }
/** * Make sure a lost group will not be deleted * * @see https://github.com/Project60/sepa_dd/issues/... * @author endres -at- systopia.de */ public function testLostGroup() { // 1) create a payment and select cycle day so that the submission would be due today $frst_notice = CRM_Core_BAO_Setting::getItem('SEPA Direct Debit Preferences', 'batching_FRST_notice'); $this->assertNotEmpty($frst_notice, "No FRST notice period specified!"); CRM_Core_BAO_Setting::setItem('SEPA Direct Debit Preferences', 'batching_RCUR_notice', $frst_notice); $rcur_notice = CRM_Core_BAO_Setting::getItem('SEPA Direct Debit Preferences', 'batching_RCUR_notice'); $this->assertNotEmpty($rcur_notice, "No RCUR notice period specified!"); $this->assertEquals($frst_notice, $rcur_notice, "Notice periods should be the same."); $cycle_day = date("d", strtotime("+{$frst_notice} days")); // 2) create a FRST mandate, due for collection right now $mandate = $this->createMandate(array('type' => 'RCUR', 'status' => 'FRST'), array('cycle_day' => $cycle_day)); $mandate_before_batching = $this->callAPISuccess("SepaMandate", "getsingle", array("id" => $mandate['id'])); $this->assertTrue($mandate_before_batching['status'] == 'FRST', "Mandate was not created in the correct status."); // 3) batch and find and check the created contribution CRM_Sepa_Logic_Batching::updateRCUR($mandate['creditor_id'], 'FRST'); $contribution_recur_id = $mandate['entity_id']; $this->assertNotEmpty($contribution_recur_id, "No entity set in mandate"); $contribution_id = CRM_Core_DAO::singleValueQuery("SELECT id FROM civicrm_contribution WHERE contribution_recur_id={$contribution_recur_id};"); $this->assertNotEmpty($contribution_id, "Couldn't find created contribution."); $this->assertDBQuery(1, "SELECT count(id) FROM civicrm_contribution WHERE contribution_recur_id={$contribution_recur_id};"); $this->assertDBQuery(1, "SELECT count(txgroup_id) FROM civicrm_sdd_contribution_txgroup WHERE contribution_id={$contribution_id};"); $txgroup_id = CRM_Core_DAO::singleValueQuery("SELECT txgroup_id FROM civicrm_sdd_contribution_txgroup WHERE contribution_id={$contribution_id};"); $txgroup = $this->callAPISuccess("SepaTransactionGroup", "getsingle", array("id" => $txgroup_id)); $latest_submission_date = explode(' ', $txgroup['latest_submission_date']); $this->assertEquals(date('Y-m-d'), $latest_submission_date[0], "Something went wrong, this group should be due today!"); // 4) set the grace period to 7 and virtually execute batching for the day after tomorrow // => the group should be retained CRM_Sepa_Logic_Settings::setSetting('batching.RCUR.grace', '7'); $grace_period = (int) CRM_Sepa_Logic_Settings::getSetting("batching.RCUR.grace", $mandate['creditor_id']); $this->assertEquals(7, $grace_period, "Setting the grace period failed!"); CRM_Sepa_Logic_Batching::updateRCUR($mandate['creditor_id'], 'FRST', date('Y-m-d', strtotime("+3 day"))); $txgroups = $this->callAPISuccess("SepaTransactionGroup", "get", array("id" => $txgroup_id)); $this->assertEquals(1, $txgroups['count'], "transaction group was deleted!"); // 5) set the grace period to 0 and virtually execute batching for the day after tomorrow // => the group should be deleted CRM_Sepa_Logic_Settings::setSetting('batching.RCUR.grace', '0'); $grace_period = (int) CRM_Sepa_Logic_Settings::getSetting("batching.RCUR.grace", $mandate['creditor_id']); $this->assertEquals(0, $grace_period, "Setting the grace period failed!"); CRM_Sepa_Logic_Batching::updateRCUR($mandate['creditor_id'], 'FRST', date('Y-m-d', strtotime("+3 day"))); $txgroups = $this->callAPISuccess("SepaTransactionGroup", "get", array("id" => $txgroup_id)); $this->assertEquals(0, $txgroups['count'], "transaction group was not deleted!"); }
/** * This method will adjust the collection date, * so it can still be submitted by the give submission date * * @param txgroup_id the transaction group for which the file should be created * @param latest_submission_date the date when it should be submitted * * @return an update array with the txgroup or a string with an error message */ static function adjustCollectionDate($txgroup_id, $latest_submission_date) { $txgroup = civicrm_api('SepaTransactionGroup', 'getsingle', array('version' => 3, 'id' => $txgroup_id)); if (!empty($txgroup['is_error'])) { return $txgroup['error_message']; } $test_date_parse = strtotime($latest_submission_date); if (empty($test_date_parse)) { return "Bad date adjustment given!"; } $notice_period = (int) CRM_Sepa_Logic_Settings::getSetting("batching.{$txgroup['type']}.notice", $txgroup['sdd_creditor_id']); $new_collection_date = date('YmdHis', strtotime("{$latest_submission_date} + {$notice_period} days")); $new_latest_submission_date = date('YmdHis', strtotime("{$latest_submission_date}")); $result = civicrm_api('SepaTransactionGroup', 'create', array('version' => 3, 'id' => $txgroup_id, 'collection_date' => $new_collection_date, 'latest_submission_date' => $new_latest_submission_date)); if (!empty($result['is_error'])) { return $result['error_message']; } // reload the item $txgroup = civicrm_api('SepaTransactionGroup', 'getsingle', array('version' => 3, 'id' => $txgroup_id)); if (!empty($txgroup['is_error'])) { return $txgroup['error_message']; } else { return $txgroup; } }
/** * This is the counterpart to the doDirectPayment method. This method creates * partial mandates, where the subsequent payment processess produces a payment. * * This function here should be called after the payment process was completed. * It will process all the PARTIAL mandates and connect them with created contributions. */ public static function processPartialMandates() { // load all the PARTIAL mandates $partial_mandates = civicrm_api3('SepaMandate', 'get', array('version' => 3, 'status' => 'PARTIAL', 'option.limit' => 9999)); foreach ($partial_mandates['values'] as $mandate_id => $mandate) { if ($mandate['type'] == 'OOFF') { // in the OOFF case, we need to find the contribution, and connect it $contribution = civicrm_api('Contribution', 'getsingle', array('version' => 3, 'trxn_id' => $mandate['reference'])); if (empty($contribution['is_error'])) { // check collection date $ooff_notice = (int) CRM_Sepa_Logic_Settings::getSetting("batching.OOFF.notice", $mandate['creditor_id']); $first_collection_date = strtotime("+{$ooff_notice} days"); $collection_date = strtotime($contribution['receive_date']); if ($collection_date < $first_collection_date) { // adjust collection date to the earliest possible one $collection_date = $first_collection_date; } // FOUND! Update the contribution... $contribution_bao = new CRM_Contribute_BAO_Contribution(); $contribution_bao->get('id', $contribution['id']); $contribution_bao->is_pay_later = 0; $contribution_bao->receive_date = date('YmdHis', $collection_date); $contribution_bao->contribution_status_id = (int) CRM_Core_OptionGroup::getValue('contribution_status', 'Pending', 'name'); $contribution_bao->payment_instrument_id = (int) CRM_Core_OptionGroup::getValue('payment_instrument', 'OOFF', 'name'); $contribution_bao->save(); // ...and connect it to the mandate $mandate_update = array(); $mandate_update['id'] = $mandate['id']; $mandate_update['entity_id'] = $contribution['id']; $mandate_update['type'] = $mandate['type']; if (empty($mandate['contact_id'])) { // this happens when the payment gets created AFTER the doDirectPayment method $mandate_update['contact_id'] = $contribution_bao->contact_id; } // initialize according to the creditor settings CRM_Sepa_BAO_SEPACreditor::initialiseMandateData($mandate['creditor_id'], $mandate_update); // finally, write the changes to the mandate civicrm_api3('SepaMandate', 'create', $mandate_update); } else { // if NOT FOUND or error, delete the partial mandate civicrm_api3('SepaMandate', 'delete', array('id' => $mandate_id)); } } elseif ($mandate['type'] == 'RCUR') { // in the RCUR case, we also need to find the contribution, and connect it // load the contribution AND the associated recurring contribution $contribution = civicrm_api('Contribution', 'getsingle', array('version' => 3, 'trxn_id' => $mandate['reference'])); $rcontribution = civicrm_api('ContributionRecur', 'getsingle', array('version' => 3, 'trxn_id' => $mandate['reference'])); if (empty($contribution['is_error']) && empty($rcontribution['is_error'])) { // we need to set the receive date to the correct collection date, otherwise it will be created again (w/o) $rcur_notice = (int) CRM_Sepa_Logic_Settings::getSetting("batching.RCUR.notice", $mandate['creditor_id']); $now = strtotime(date('Y-m-d', strtotime("now +{$rcur_notice} days"))); // round to full day $collection_date = CRM_Sepa_Logic_Batching::getNextExecutionDate($rcontribution, $now); // fix contribution $contribution_bao = new CRM_Contribute_BAO_Contribution(); $contribution_bao->get('id', $contribution['id']); $contribution_bao->is_pay_later = 0; $contribution_bao->contribution_status_id = (int) CRM_Core_OptionGroup::getValue('contribution_status', 'Pending', 'name'); $contribution_bao->payment_instrument_id = (int) CRM_Core_OptionGroup::getValue('payment_instrument', 'FRST', 'name'); $contribution_bao->receive_date = date('YmdHis', strtotime($collection_date)); $contribution_bao->save(); // fix recurring contribution $rcontribution_bao = new CRM_Contribute_BAO_ContributionRecur(); $rcontribution_bao->get('id', $rcontribution['id']); $rcontribution_bao->start_date = date('YmdHis', strtotime($rcontribution_bao->start_date)); $rcontribution_bao->create_date = date('YmdHis', strtotime($rcontribution_bao->create_date)); $rcontribution_bao->modified_date = date('YmdHis', strtotime($rcontribution_bao->modified_date)); $rcontribution_bao->contribution_status_id = (int) CRM_Core_OptionGroup::getValue('contribution_status', 'Pending', 'name'); $rcontribution_bao->payment_instrument_id = (int) CRM_Core_OptionGroup::getValue('payment_instrument', 'FRST', 'name'); $rcontribution_bao->save(); // ...and connect it to the mandate $mandate_update = array(); $mandate_update['id'] = $mandate['id']; $mandate_update['entity_id'] = $rcontribution['id']; $mandate_update['type'] = $mandate['type']; if (empty($mandate['contact_id'])) { $mandate_update['contact_id'] = $contribution['contact_id']; $mandate['contact_id'] = $contribution['contact_id']; } //NO: $mandate_update['first_contribution_id'] = $contribution['id']; // initialize according to the creditor settings CRM_Sepa_BAO_SEPACreditor::initialiseMandateData($mandate['creditor_id'], $mandate_update); // finally, write the changes to the mandate civicrm_api3('SepaMandate', 'create', $mandate_update); // ...and trigger notification // FIXME: WORKAROUND, see https://github.com/Project60/org.project60.sepa/issues/296) CRM_Contribute_BAO_ContributionPage::recurringNotify(CRM_Core_Payment::RECURRING_PAYMENT_START, $mandate['contact_id'], $contribution_bao->contribution_page_id, $rcontribution_bao); } else { // something went wrong, delete partial error_log("org.project60.sepa: deleting partial mandate " . $mandate['reference']); civicrm_api3('SepaMandate', 'delete', array('id' => $mandate_id)); } } } }
/** * runs a batching update for all OOFF mandates */ static function updateOOFF($creditor_id, $now = 'now') { // check lock $lock = CRM_Sepa_Logic_Settings::getLock(); if (empty($lock)) { return "Batching in progress. Please try again later."; } $horizon = (int) CRM_Sepa_Logic_Settings::getSetting('batching.OOFF.horizon', $creditor_id); $ooff_notice = (int) CRM_Sepa_Logic_Settings::getSetting('batching.OOFF.notice', $creditor_id); $group_status_id_open = (int) CRM_Core_OptionGroup::getValue('batch_status', 'Open', 'name'); $date_limit = date('Y-m-d', strtotime("{$now} +{$horizon} days")); // step 1: find all active/pending OOFF mandates within the horizon that are NOT in a closed batch $sql_query = "\n SELECT\n mandate.id AS mandate_id,\n mandate.contact_id AS mandate_contact_id,\n mandate.entity_id AS mandate_entity_id,\n contribution.receive_date AS start_date\n FROM civicrm_sdd_mandate AS mandate\n INNER JOIN civicrm_contribution AS contribution ON mandate.entity_id = contribution.id\n WHERE contribution.receive_date <= DATE('{$date_limit}')\n AND mandate.type = 'OOFF'\n AND mandate.status = 'OOFF'\n AND mandate.creditor_id = {$creditor_id};"; $results = CRM_Core_DAO::executeQuery($sql_query); $relevant_mandates = array(); while ($results->fetch()) { // TODO: sanity checks? $relevant_mandates[$results->mandate_id] = array('mandate_id' => $results->mandate_id, 'mandate_contact_id' => $results->mandate_contact_id, 'mandate_entity_id' => $results->mandate_entity_id, 'start_date' => $results->start_date); } // step 2: group mandates in collection dates $calculated_groups = array(); $earliest_collection_date = date('Y-m-d', strtotime("{$now} +{$ooff_notice} days")); $latest_collection_date = ''; foreach ($relevant_mandates as $mandate_id => $mandate) { $collection_date = date('Y-m-d', strtotime($mandate['start_date'])); if ($collection_date <= $earliest_collection_date) { $collection_date = $earliest_collection_date; } if (!isset($calculated_groups[$collection_date])) { $calculated_groups[$collection_date] = array(); } array_push($calculated_groups[$collection_date], $mandate); if ($collection_date > $latest_collection_date) { $latest_collection_date = $collection_date; } } if (!$latest_collection_date) { // nothing to do... return array(); } // step 3: find all existing OPEN groups in the horizon $sql_query = "\n SELECT\n txgroup.collection_date AS collection_date,\n txgroup.id AS txgroup_id\n FROM civicrm_sdd_txgroup AS txgroup\n WHERE txgroup.collection_date <= '{$latest_collection_date}'\n AND txgroup.sdd_creditor_id = {$creditor_id}\n AND txgroup.type = 'OOFF'\n AND txgroup.status_id = {$group_status_id_open};"; $results = CRM_Core_DAO::executeQuery($sql_query); $existing_groups = array(); while ($results->fetch()) { $collection_date = date('Y-m-d', strtotime($results->collection_date)); $existing_groups[$collection_date] = $results->txgroup_id; } // step 4: sync calculated group structure with existing (open) groups self::syncGroups($calculated_groups, $existing_groups, 'OOFF', 'OOFF', $ooff_notice, $creditor_id); $lock->release(); }
/** * Reads the default creditor from the settings * Will only return a creditor if it exists and if it's active * * @return CRM_Sepa_BAO_SEPACreditor object or NULL */ static function defaultCreditor() { $default_creditor_id = (int) CRM_Sepa_Logic_Settings::getSetting('batching_default_creditor'); if (empty($default_creditor_id)) { return NULL; } $default_creditor = new CRM_Sepa_DAO_SEPACreditor(); $default_creditor->get('id', $default_creditor_id); if (empty($default_creditor->mandate_active)) { return NULL; } else { return $default_creditor; } }
/** * Test civicrm_api3_sepa_mandate_create with default creditor * * @author niko bochan */ public function testCreateWithDefaultCreditor() { $contactId = $this->individualCreate(); $contribution = $this->callAPISuccess("Contribution", "create", array('version' => 3, 'financial_type_id' => 1, 'contribution_status_id' => 2, 'total_amount' => '100.00', 'currency' => 'EUR', 'contact_id' => $contactId)); $create_data = array('version' => 3, 'type' => 'OOFF', 'status' => 'INIT', 'entity_id' => $contribution['id'], 'entity_table' => 'civicrm_contribution', 'contact_id' => $contactId, 'start_date' => date('YmdHis'), 'receive_date' => date('YmdHis'), 'date' => date('YmdHis'), 'iban' => "DE12500105170648489890", 'bic' => "COLSDE33XXX", 'is_enabled' => 1); // set default creditor $creditor_id = $this->getCreditor(); CRM_Sepa_Logic_Settings::setSetting('batching_default_creditor', $creditor_id); $create_data['creditor_id'] = $creditor_id; // this should work, since no creditor_id is set BUT the default creditor is $this->callAPISuccess("SepaMandate", "create", $create_data); // set bad default creditor CRM_Sepa_Logic_Settings::setSetting('batching_default_creditor', '999'); unset($create_data['creditor_id']); $this->assertEquals('999', CRM_Sepa_Logic_Settings::getSetting('batching_default_creditor')); // this should fail, since no creditor_id is set and the default creditor is bad $this->callAPIFailure("SepaMandate", "create", $create_data); }