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);
 }