webform_civicrm-8.x-5.0-beta3/src/WebformCivicrmPostProcess.php
src/WebformCivicrmPostProcess.php
<?php
namespace Drupal\webform_civicrm;
/**
* @file
* Front-end form validation and post-processing.
*/
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\webform\WebformInterface;
use Drupal\webform\WebformSubmissionInterface;
use Drupal\webform_civicrm\WebformCivicrmBase;
class WebformCivicrmPostProcess extends WebformCivicrmBase implements WebformCivicrmPostProcessInterface {
// Variables used during validation
/**
* @var array
*/
private $form;
/**
* @var \Drupal\Core\Form\FormStateInterface
*/
private $form_state;
private $crmValues;
private $rawValues;
private $multiPageDataLoaded;
private $billing_params = [];
private $totalContribution = 0;
private $contributionIsIncomplete = FALSE;
private $contributionIsPayLater = FALSE;
// During validation this contains an array of known contact ids and the placeholder 0 for valid contacts
// During submission processing this contains an array of known contact ids
private $existing_contacts = [];
// Variables used during submission processing
/**
* @var \Drupal\Core\Database\Connection
*/
private $database;
/**
* @var \Drupal\webform_civicrm\Plugin\WebformHandler
*/
private $handler;
/**
* @var \Drupal\webform\WebformSubmissionInterface
*/
private $submission;
/**
* @var \Drupal\webform_civicrm\UtilsInterface
*/
protected $utils;
private $all_fields;
private $all_sets;
private $shared_address = [];
/**
* Static cache.
*
* @var bool
*/
protected $initialized = FALSE;
const
BILLING_MODE_LIVE = 1,
BILLING_MODE_MIXED = 3;
public function __construct(UtilsInterface $utils) {
$this->utils = $utils;
}
function initialize(WebformSubmissionInterface $webform_submission) {
if ($this->initialized) {
return $this;
}
$this->node = $webform_submission->getWebform();
$handler_collection = $this->node->getHandlers('webform_civicrm');
$instance_ids = $handler_collection->getInstanceIds();
$this->handler = $handler_collection->get(reset($instance_ids));
$this->database = \Drupal::database();
$this->settings = $this->handler->getConfiguration()['settings'];
$this->data = $this->settings['data'];
$this->enabled = $this->utils->wf_crm_enabled_fields($this->node);
$this->all_fields = $this->utils->wf_crm_get_fields();
$this->all_sets = $this->utils->wf_crm_get_fields('sets');
$this->initialized = TRUE;
return $this;
}
/**
* Called after a webform is submitted
* Or, for a multipage form, called after each page
* @param array $form
* @param FormStateInterface $form_state
*/
public function validate($form, FormStateInterface $form_state) {
$this->form = $form;
$this->form_state = $form_state;
$this->rawValues = $form_state->getValues();
$this->crmValues = $this->utils->wf_crm_enabled_fields($this->node, $this->rawValues);
// Even though this object is destroyed between page submissions, this trick allows us to persist some data - see below
$this->ent = $form_state->get(['civicrm', 'ent']) ?: $this->ent;
$errors = $this->form_state->getErrors();
foreach ($errors as $key => $error) {
$pieces = $this->utils->wf_crm_explode_key(substr($key, strrpos($key, '][') + 2));
if ($pieces) {
[ , $c, $ent, $n, $table, $name] = $pieces;
if ($this->isFieldHiddenByExistingContactSettings($ent, $c, $table, $n, $name)) {
$this->unsetError($key);
}
elseif ($table === 'address' && !empty($this->crmValues["civicrm_{$c}_contact_{$n}_address_master_id"])) {
$master_id = $this->crmValues["civicrm_{$c}_contact_{$n}_address_master_id"];
// If widget is checkboxes, need to filter the array
if (!is_array($master_id) || array_filter($master_id)) {
$this->unsetError($key);
}
}
}
}
$this->validateThisPage($this->form);
if (!empty($this->data['participant']) && !empty($this->data['participant_reg_type'])) {
$this->loadMultiPageData();
$this->validateParticipants();
}
// Process live contribution. If the transaction is unsuccessful it will trigger a form validation error.
$contribution_enabled = wf_crm_aval($this->data, 'contribution:1:contribution:1:enable_contribution');
if ($contribution_enabled) {
// Ensure contribution js is still loaded if the form has to refresh
$this->addPaymentJs();
$this->loadMultiPageData();
if ($this->tallyLineItems()) {
if ($this->isLivePaymentProcessor() && $this->isPaymentPage() && !$form_state->getErrors()) {
if ($this->validateBillingFields()) {
if ($this->createBillingContact()) {
$this->submitLivePayment();
}
}
}
}
}
// Even though this object is destroyed between page submissions, this trick allows us to persist some data - see above
$form_state->set(['civicrm', 'ent'], $this->ent);
// TODO: Is line_items being set but never retrieved?
$form_state->set(['civicrm', 'line_items'], $this->line_items);
}
/**
* Modify data in webform submission. civicrm_options element's key is submitted to the delta
* column of webform_submission_data. Make sure it is not set to a string value.
*
* @param \Drupal\webform\WebformSubmissionInterface $webform_submission
*/
protected function modifyWebformSubmissionData(WebformSubmissionInterface $webform_submission) {
$data = $webform_submission->getData();
$webform = $webform_submission->getWebform();
foreach ($data as $field_key => $val) {
$element = $webform->getElement($field_key);
if ($element && $element['#type'] == 'civicrm_options' && is_array($val) && count(array_filter(array_keys($val), 'is_string')) > 0) {
$data[$field_key] = array_values($val);
}
}
$webform_submission->setData($data);
}
/**
* Process webform submission when it is about to be saved. Called by the following hook:
*
* @see webform_civicrm_webform_submission_presave
*
* @param \Drupal\webform\WebformSubmissionInterface $webform_submission
*/
public function preSave(WebformSubmissionInterface $webform_submission) {
$this->submission = $webform_submission;
$this->data = $this->settings['data'];
$this->modifyWebformSubmissionData($webform_submission);
// Fill $this->id from existing contacts
$this->getExistingContactIds();
// While saving a draft, just skip to postSave and write the record
if ($this->submission->isDraft()) {
return;
}
$this->fillDataFromSubmission();
//Fill Custom Contact reference fields.
$this->fillContactRefs(TRUE);
// Create/update contacts
foreach ($this->data['contact'] as $c => $contact) {
if (empty($this->ent['contact'][$c]['id'])) {
// Don't create contact if we don't have a name or email
if ($this->isContactEmpty($contact)) {
$this->ent['contact'][$c]['id'] = 0;
continue;
}
$this->ent['contact'][$c]['id'] = $this->findDuplicateContact($contact);
}
// Current employer must wait for ContactRef ids to be filled
unset($contact['contact'][1]['employer_id']);
$newContact = empty($this->ent['contact'][$c]['id']);
// Avoid Issue with lowercase validation of webform
if (!empty($contact['contact'][1]['image_url'])) {
$contact['contact'][1]['image_URL'] = $contact['contact'][1]['image_url'];
unset($contact['contact'][1]['image_url']);
}
// Create new contact
if ($newContact) {
$this->ent['contact'][$c]['id'] = $this->createContact($contact);
}
if ($c == 1) {
$this->setLoggingContact();
}
// Update existing contact
if (!$newContact) {
$this->updateContact($contact, $c);
}
}
// $this->ent['contact'] will now contain all contacts in order, with 0 as a placeholder id for any contact not saved
ksort($this->ent['contact']);
// Once all contacts are saved we can fill contact ref fields
$this->fillContactRefs();
foreach ($this->data['contact'] as $c => $contact) {
$cid = $this->ent['contact'][$c]['id'];
if ($cid) {
$this->saveMultiRecordCustomData($contact, $c, $cid);
}
}
// Save a non-live transaction
if ($this->totalContribution && empty($this->ent['contribution'][1]['id'])) {
$this->createDeferredPayment();
}
// Create/update other data associated with contacts
foreach ($this->data['contact'] as $c => $contact) {
$cid = $this->ent['contact'][$c]['id'];
if (!$cid) {
continue;
}
if (!empty($contact['update_contact_ref'])) {
$this->saveContactRefs($contact, $cid);
}
$this->saveCurrentEmployer($contact, $cid);
$this->fillHiddenContactFields($cid, $c);
$this->saveContactLocation($contact, $cid, $c);
$this->saveGroupsAndTags($contact, $cid, $c);
$this->saveRelationships($contact, $cid, $c);
// Process event participation
if (isset($this->all_sets['participant']) && !empty($this->data['participant_reg_type'])) {
$this->processParticipants($c, $cid);
}
}
// We do this after all contacts and addresses exist
$this->processSharedAddresses();
// Process memberships after relationships have been created
foreach ($this->ent['contact'] as $c => $contact) {
if ($contact['id'] && isset($this->all_sets['membership']) && !empty($this->data['membership'][$c]['number_of_membership'])) {
$this->processMemberships($c, $contact['id']);
}
}
}
/**
* Process webform submission after it is has been saved. Called by the following hooks:
* @see webform_civicrm_webform_submission_insert
* @see webform_civicrm_webform_submission_update
* @param stdClass $submission
*/
public function postSave(WebformSubmissionInterface $webform_submission) {
$this->submission = $webform_submission;
if (empty($this->submission->isDraft())) {
// Save cases
if (!empty($this->data['case']['number_of_case'])) {
$this->processCases();
}
// Save activities
if (!empty($this->data['activity']['number_of_activity'])) {
$this->processActivities();
}
// Save grants
if (isset($this->data['grant']['number_of_grant'])) {
$this->processGrants();
}
// Save contribution line-items
if (!empty($this->ent['contribution'][1]['id'])) {
$this->processContribution();
}
}
// Write record; we do this when creating, updating, or saving a draft of a webform submission.
$record = $this->formatSubmission();
$this->database->merge('webform_civicrm_submissions')
->key('sid', $record['sid'])
->fields($record)
->execute();
// Calling an IPN payment processor will result in a redirect so this happens after everything else
if ($this->contributionIsIncomplete && !$this->contributionIsPayLater && !empty($this->ent['contribution'][1]['id']) && !$this->submission->isDraft()) {
// webform_submission_send_mail($this->node, $this->submission);
$this->submitIPNPayment();
}
$isEmailReceipt = wf_crm_aval($this->data, "receipt:number_number_of_receipt", FALSE);
// Send receipt
if (empty($this->submission->isDraft())
&& !empty($this->ent['contribution'][1]['id'])
&& !empty($isEmailReceipt)
&& (!$this->contributionIsIncomplete || $this->contributionIsPayLater)
) {
$this->sendReceipt();
}
}
/**
* Send receipt
*/
private function sendReceipt() {
// tax integration
if (!is_null($this->tax_rate)) {
$dataArray = [];
$totalTaxAmount = NULL;
foreach ($this->line_items as $key => $value) {
if (isset($value['tax_amount']) && isset($value['tax_rate'])) {
if (isset($dataArray[$value['tax_rate']])) {
$dataArray[$value['tax_rate']] = $dataArray[$value['tax_rate']] + $value['tax_amount'];
}
else {
$dataArray[$value['tax_rate']] = $value['tax_amount'];
}
$totalTaxAmount += $value['tax_amount'];
}
}
$template = \CRM_Core_Smarty::singleton();
$template->assign('dataArray', $dataArray);
$template->assign('totalTaxAmount', $totalTaxAmount);
}
if ($this->contributionIsIncomplete) {
$template = \CRM_Core_Smarty::singleton();
$template->assign('is_pay_later', 1);
}
$this->utils->wf_civicrm_api('contribution', 'sendconfirmation', $this->utils->getReceiptParams($this->data, $this->ent['contribution'][1]['id']));
}
/**
* Formats submission data as expected by the schema
*/
private function formatSubmission() {
$data = $this->ent;
$record = [
'sid' => $this->submission->id(),
'contact_id' => '-',
'civicrm_data' => serialize($data),
];
foreach ($this->ent['contact'] as $contact) {
$record['contact_id'] .= $contact['id'] . '-';
}
return $record;
}
/**
* Recursive validation callback for webform page submission
*
* @todo this shouldn't be needed as each element should handle this.
*
* @param array $elements
* FAPI form array
*/
private function validateThisPage($elements) {
// Recurse through form elements.
foreach (Element::children($elements) as $key) {
if (is_array($elements[$key]) && ($element = $elements[$key])) {
$this->validateThisPage($elements[$key]);
if (empty($element['#civicrm_data_type'])) {
continue;
}
if (!isset($element['#value']) || $element['#value'] === '') {
continue;
}
$element_type = $element['#type'] ?? '';
if (strpos($element_type, 'text') !== 0) {
continue;
}
$dt = $element['#civicrm_data_type'];
// Validate state/prov abbreviation
if ($dt === 'state_province_abbr') {
$ckey = str_replace('state_province', 'country', $key);
if (!empty($this->crmValues[$ckey]) && is_numeric($this->crmValues[$ckey])) {
$country_id = $this->crmValues[$ckey];
}
else {
$country_id = (int) $this->utils->wf_crm_get_civi_setting('defaultContactCountry', 1228);
}
$states = $this->utils->wf_crm_get_states($country_id);
if ($states && !array_key_exists(strtoupper($element['#value']), $states)) {
$countries = $this->utils->wf_crm_apivalues('address', 'getoptions', ['field' => 'country_id']);
$this->form_state->setError($element, t('Mismatch: "@state" is not a state/province of %country. Please enter a valid state/province abbreviation for %field.', ['@state' => $element['#value'], '%country' => $countries[$country_id], '%field' => $element['#title']]));
}
}
// Strings and files don't need any validation
elseif ($dt !== 'String' && $dt !== 'Memo' && $dt !== 'File'
&& \CRM_Utils_Type::escape($element['#value'], $dt, FALSE) === NULL) {
// Allow data type names to be translated
switch ($dt) {
case 'Int':
$dt = t('an integer');
break;
case 'Float':
$dt = t('a number');
break;
case 'Link':
$dt = t('a web address starting with http://');
break;
case 'Money':
$dt = t('a currency value');
break;
}
$this->form_state->setError($element, t('Please enter @type for %field.', ['@type' => $dt, '%field' => $element['#title']]));
}
}
}
}
/**
* Validate event participants and add line items
*/
private function validateParticipants() {
// If we have no valid contacts on the form, don't bother continuing
if (!$this->existing_contacts) {
return;
}
$count = $this->data['participant_reg_type'] == 'all' ? count($this->existing_contacts) : 1;
// Collect selected events
foreach ($this->data['participant'] as $c => $par) {
if ($this->data['participant_reg_type'] == 'all') {
$contacts = $this->existing_contacts;
}
elseif (isset($this->existing_contacts[$c])) {
$contacts = [$this->existing_contacts[$c]];
}
else {
continue;
}
$participants = current(wf_crm_aval($this->data['contact'], "$c:contact", []));
$participantName = ($participants['first_name'] ?? '') . ' ' . ($participants['last_name'] ?? '');
if (!trim($participantName)) {
$participantName = $participants['webform_label'] ?? NULL;
if (!empty($this->existing_contacts[$c])) {
$participantName = $this->utils->wf_crm_apivalues('contact', 'get', $this->existing_contacts[$c], 'display_name')[0];
}
}
// @todo this duplicates a lot in \wf_crm_webform_preprocess::populateEvents
foreach (wf_crm_aval($par, 'participant', []) as $n => $p) {
foreach (array_filter(wf_crm_aval($p, 'event_id', [])) as $id_and_type) {
[$eid] = explode('-', $id_and_type);
if (is_numeric($eid)) {
$this->events[$eid]['ended'] = TRUE;
$this->events[$eid]['title'] = t('this event');
$this->events[$eid]['count'] = wf_crm_aval($this->events, "$eid:count", 0) + $count;
$this->line_items[] = [
'qty' => $count,
'entity_table' => 'civicrm_participant',
'event_id' => $eid,
'contact_ids' => $contacts,
'unit_price' => $p['fee_amount'] ?? 0,
'element' => "civicrm_{$c}_participant_{$n}_participant_{$id_and_type}",
'contact_label' => $participantName,
];
}
}
}
}
// Subtract events already registered for - this only works with known contacts
$cids = array_filter($this->existing_contacts);
if ($this->events && $cids) {
$status_types = $this->utils->wf_crm_apivalues('participant_status_type', 'get', ['is_counted' => 1, 'return' => 'id']);
$existing = $this->utils->wf_crm_apivalues('Participant', 'get', [
'return' => ['event_id', 'contact_id'],
'event_id' => ['IN' => array_keys($this->events)],
'status_id' => ['IN' => array_keys($status_types)],
'contact_id' => ['IN' => $cids],
'is_test' => 0,
]);
foreach ($existing as $participant) {
foreach ($this->line_items as $k => &$item) {
if ($participant['event_id'] == $item['event_id'] && in_array($participant['contact_id'], $item['contact_ids'])) {
unset($this->line_items[$k]['contact_ids'][array_search($participant['contact_id'], $item['contact_ids'])]);
if (!(--$item['qty'])) {
unset($this->line_items[$k]);
}
// Update the count in $this->events elements.
if (!(--$this->events[$participant['event_id']]['count'])) {
unset($this->events[$participant['event_id']]);
}
}
}
}
}
$this->loadEvents();
// Add event info to line items
$format = wf_crm_aval($this->data['reg_options'], 'title_display', 'title');
// Get the Event Fee Financial Type Id if active, otherwise get the first active Financial Type Id
if ($this->line_items) {
$eventFTId = $this->utils->wf_crm_apivalues('FinancialType', 'get', [
'sequential' => 1,
'return' => ["id"],
'name' => "Event Fee",
'is_active' => 1,
])[0]['id'] ?? NULL;
if (empty($eventFTId)) {
$eventFTId = $this->utils->wf_crm_apivalues('FinancialType', 'get', [
'sequential' => 1,
'return' => ["id"],
'is_active' => 1,
'options' => ['limit' => 1],
])[0]['id'];
}
}
foreach ($this->line_items as &$item) {
$label = empty($item['contact_label']) ? '' : "{$item['contact_label']} - ";
$item['label'] = $label . $this->utils->wf_crm_format_event($this->events[$item['event_id']], $format);
$item['financial_type_id'] = wf_crm_aval($this->events[$item['event_id']], 'financial_type_id', $eventFTId);
}
// Form Validation
if (!empty($this->data['reg_options']['validate'])) {
foreach ($this->events as $eid => $event) {
if ($event['ended']) {
$this->form_state->setErrorByName($eid, t('Sorry, you can no longer register for %event.', ['%event' => $event['title']]));
}
elseif (!empty($event['max_participants']) && $event['count'] > $event['available_places']) {
if (!empty($event['is_full'])) {
$this->form_state->setErrorByName($eid, t('%event : @text', ['%event' => $event['title'], '@text' => $event['event_full_text']]));
}
else {
$registrations = \Drupal::translation()->formatPlural($event['count'], '1 person', '@count people');
$this->form_state->setErrorByName($eid, \Drupal::translation()->formatPlural($event['available_places'],
'Sorry, you tried to register @registrations for %event but there is only 1 space remaining.',
'Sorry, you tried to register @registrations for %event but there are only @count spaces remaining.',
['%event' => $event['title'], '@registrations' => $registrations]));
}
}
}
}
}
/**
* Load entire webform submission during validation, including contact ids and $this->data
* Used when validation for one page needs access to submitted values from other pages
*/
private function loadMultiPageData() {
if (!$this->multiPageDataLoaded) {
$this->multiPageDataLoaded = TRUE;
/*
* This feels like an old/weird hack to bypass form API/webform funk in D7.
if (!empty($this->form_state['storage']['submitted']) && wf_crm_aval($this->form_state, 'storage:page_num', 1) > 1) {
$this->rawValues += $this->form_state['storage']['submitted'];
$this->crmValues = wf_crm_enabled_fields($this->node, $this->rawValues);
}
*/
// Check how many valid contacts we have
foreach ($this->data['contact'] as $c => $contact) {
// Check if we have a contact_id
$fid = "civicrm_{$c}_contact_1_contact_existing";
if ($this->verifyExistingContact(wf_crm_aval($this->crmValues, $fid), $fid)) {
$this->existing_contacts[$c] = $this->crmValues[$fid];
}
// Or else see if enough info was entered to create a contact - use 0 as a placeholder for unknown cid
elseif ($this->utils->wf_crm_name_field_exists($this->crmValues, $c, $contact['contact'][1]['contact_type'])) {
$this->existing_contacts[$c] = 0;
}
}
// Fill data array with submitted form values
$this->fillDataFromSubmission();
}
}
/**
* Fetch contact ids from "existing contact" fields
*/
private function getExistingContactIds() {
foreach ($this->enabled as $field_key => $fid) {
if (substr($field_key, -8) === 'existing') {
[, $c, ] = explode('_', $field_key, 3);
$cid = wf_crm_aval($this->submissionValue($fid), 0);
$this->ent['contact'][$c]['id'] = $this->verifyExistingContact($cid, $field_key);
if ($this->ent['contact'][$c]['id']) {
$this->existing_contacts[$c] = $cid;
}
}
}
}
/**
* Ensure we have a valid contact id in a contact ref field
* @param $cid
* @param $fid
* @return int
*/
private function verifyExistingContact($cid, $fid) {
if ($this->utils->wf_crm_is_positive($cid) && !empty($this->enabled[$fid])) {
$contactComponent = \Drupal::service('webform_civicrm.contact_component');
$component = $this->node->getElement($fid);
$filters = $contactComponent->wf_crm_search_filters($this->node, $component);
// Verify access to this contact
if ($contactComponent->wf_crm_contact_access($component, $filters, $cid) !== FALSE) {
return $cid;
}
}
return 0;
}
/**
* Check if at least one required field was filled for a contact
* @param array $contact
* @return bool
*/
private function isContactEmpty($contact) {
$contact_type = $contact['contact'][1]['contact_type'];
foreach ($this->utils->wf_crm_required_contact_fields($contact_type) as $f) {
if (!empty($contact[$f['table']][1][$f['name']])) {
return FALSE;
}
}
return TRUE;
}
/**
* Search for an existing contact using configured deupe rule
* @param array $contact
* @return int
*/
private function findDuplicateContact($contact) {
// This is either a default type (Unsupervised or Supervised) or the id of a specific rule
$rule = wf_crm_aval($contact, 'matching_rule', 'Unsupervised', TRUE);
if ($rule) {
$contact['contact'][1]['contact_type'] = ucfirst($contact['contact'][1]['contact_type']);
$params = [
'check_permissions' => FALSE,
'sequential' => TRUE,
'rule_type' => is_numeric($rule) ? NULL : $rule,
'dedupe_rule_id' => is_numeric($rule) ? $rule : NULL,
'match' => [],
];
// If sharing an address, use the master
if (!empty($contact['address'][1]['master_id'])) {
$m = $contact['address'][1]['master_id'];
// If master address is exposed to the form, use it
if (!empty($this->data['contact'][$m]['address'][1])) {
$contact['address'][1] = $this->data['contact'][$m]['address'][1];
}
// Else look up the master contact's address
elseif (!empty($this->existing_contacts[$m])) {
$masters = $this->utils->wf_crm_apivalues('address', 'get', [
'contact_id' => $this->ent['contact'][$m]['id'],
'sort' => 'is_primary DESC',
'limit' => 1,
]);
if (!empty($masters)) {
$contact['address'][1] = reset($masters);
}
}
}
foreach ($contact as $table => $fields) {
if (is_array($fields) && !empty($fields[1])) {
$params['match'] += $fields[1];
}
}
// Pass custom params to deduper
if ($params['match']) {
$dupes = $this->utils->wf_crm_apivalues('Contact', 'duplicatecheck', $params);
}
}
return $dupes[0]['id'] ?? 0;
}
/**
* Create a new contact
* @param array $contact
* @return int
*/
private function createContact($contact) {
$params = $contact['contact'][1];
// CiviCRM API is too picky about this, imho
$params['contact_type'] = ucfirst($params['contact_type']);
unset($params['contact_id'], $params['id']);
if (!isset($params['source'])) {
$params['source'] = $this->settings['new_contact_source'];
}
// If creating individual with no first/last name,
// set display name and sort_name
if ($params['contact_type'] == 'Individual' && empty($params['first_name']) && empty($params['last_name'])) {
$params['display_name'] = $params['sort_name'] = empty($params['nick_name']) ? $contact['email'][1]['email'] : $params['nick_name'];
}
$result = $this->utils->wf_civicrm_api('contact', 'create', $params);
return wf_crm_aval($result, 'id', 0);
}
/**
* Update a contact
* @param array $contact
* @param int $c
*/
private function updateContact($contact, $c) {
$params = $contact['contact'][1];
unset($params['contact_type'], $params['contact_id']);
// Fetch data from existing multivalued fields
$fetch = $multi = [];
foreach ($this->all_fields as $fid => $field) {
if (!empty($field['extra']['multiple']) && substr($fid, 0, 7) == 'contact') {
[, $name] = explode('_', $fid, 2);
if ($name != 'privacy' && isset($params[$name])) {
$fetch["return.$name"] = 1;
$multi[] = $name;
}
}
}
// Merge data from existing multivalued fields
if ($multi) {
$existing = $this->utils->wf_civicrm_api('contact', 'get', ['id' => $this->ent['contact'][$c]['id']] + $fetch);
$existing = wf_crm_aval($existing, 'values:' . $this->ent['contact'][$c]['id'], []);
foreach ($multi as $name) {
$exist_to_merge = (array) wf_crm_aval($existing, $name, []);
$exist = array_filter(array_combine($exist_to_merge, $exist_to_merge));
// Only known contacts are allowed to empty a field
if (!empty($this->existing_contacts[$c])) {
foreach ($this->getExposedOptions("civicrm_{$c}_contact_1_contact_$name") as $k => $v) {
unset($exist[$k]);
}
}
$params[$name] = array_unique(array_map("strtolower", array_merge($params[$name], $exist)));
}
}
$params['id'] = $this->ent['contact'][$c]['id'];
$this->utils->wf_civicrm_api('contact', 'create', $params);
}
/**
* Save current employer for a contact
* @param array $contact
* @param int $cid
*/
function saveCurrentEmployer($contact, $cid) {
if ($contact['contact'][1]['contact_type'] == 'individual' && !empty($contact['contact'][1]['employer_id'])) {
$this->utils->wf_civicrm_api('contact', 'create', [
'id' => $cid,
'employer_id' => $contact['contact'][1]['employer_id'],
]);
}
}
/**
* Fill values for hidden ID & CS fields
* @param int $c
* @param int $cid
*/
private function fillHiddenContactFields($cid, $c) {
$fid = 'civicrm_' . $c . '_contact_1_contact_';
if (!empty($this->enabled[$fid . 'contact_id'])) {
$this->submissionValue($this->enabled[$fid . 'contact_id'], $cid);
}
if (!empty($this->enabled[$fid . 'user_id'])) {
$user_id = $this->utils->wf_crm_user_cid($cid, 'contact');
$user_id = $user_id ? $user_id : '';
$this->submissionValue($this->enabled[$fid . 'user_id'], $user_id);
}
if (!empty($this->enabled[$fid . 'existing'])) {
$this->submissionValue($this->enabled[$fid . 'existing'], $cid);
}
if (!empty($this->enabled[$fid . 'external_identifier']) && !empty($this->existing_contacts[$c])) {
$exid = $this->utils->wf_civicrm_api('contact', 'get', ['contact_id' => $cid, 'return.external_identifier' => 1]);
$this->submissionValue($this->enabled[$fid . 'external_identifier'], wf_crm_aval($exid, "values:$cid:external_identifier"));
}
if (!empty($this->enabled[$fid . 'cs'])) {
$cs = $this->submissionValue($this->enabled[$fid . 'cs']);
$life = !empty($cs[0]) ? intval(24 * $cs[0]) : 'inf';
$cs = \CRM_Contact_BAO_Contact_Utils::generateChecksum($cid, NULL, $life);
$this->submissionValue($this->enabled[$fid . 'cs'], $cs);
}
}
/**
* Save location data for a contact
* @param array $contact
* @param int $cid
* @param int $c
*/
private function saveContactLocation($contact, $cid, $c) {
// Check which location_type_id is to be set as is_primary=1;
$is_primary_address_location_type = wf_crm_aval($contact, 'address:1:location_type_id');
$is_primary_email_location_type = wf_crm_aval($contact, 'email:1:location_type_id');
foreach ($this->utils->wf_crm_location_fields() as $location) {
if (!empty($contact[$location])) {
$existing = [];
$params = ['contact_id' => $cid];
if ($location != 'website') {
$params['options'] = ['sort' => 'is_primary DESC'];
}
// If billing address is submitted during validation phase, ignore updating that value.
if ($location == 'address' && !empty($this->billing_params['billing_address_id'])) {
$params['id'] = ['!=' => $this->billing_params['billing_address_id']];
}
$result = $this->utils->wf_civicrm_api($location, 'get', $params);
if (!empty($result['values'])) {
$contact[$location] = self::reorderLocationValues($contact[$location], $result['values'], $location);
// start array index at 1
$existing = array_merge([[]], $result['values']);
}
foreach ($contact[$location] as $i => $params) {
// Substitute county stub ('-' is a hack to get around required field when there are no available counties)
if (isset($params['county_id']) && $params['county_id'] === '-') {
$params['county_id'] = '';
}
// Check if anything was changed, else skip the update
if (!empty($existing[$i])) {
$same = TRUE;
foreach ($params as $param => $val) {
if ($val != (string) wf_crm_aval($existing[$i], $param, '')) {
$same = FALSE;
}
}
if ($same) {
continue;
}
}
if ($location == 'address') {
// Store shared addresses for later since we haven't necessarily processed
// the contact this address is shared with yet.
if (!empty($params['master_id'])) {
$this->shared_address[$cid][$i] = [
'id' => wf_crm_aval($existing, "$i:id"),
'mc' => $params['master_id'],
'loc' => $params['location_type_id'],
'pri' => $params['location_type_id'] == $is_primary_address_location_type,
];
continue;
}
// Reset calculated values when updating an address
$params['master_id'] = $params['geo_code_1'] = $params['geo_code_2'] = 'null';
}
$params['contact_id'] = $cid;
if (!empty($existing[$i])) {
$params['id'] = $existing[$i]['id'];
}
if ($this->locationIsEmpty($location, $params)) {
// Delete this location if nothing was entered and this is a known contact
if (!empty($this->existing_contacts[$c]) && !empty($params['id'])) {
$this->utils->wf_civicrm_api($location, 'delete', $params);
}
continue;
}
if ($location != 'website') {
if (empty($params['location_type_id'])) {
$params['location_type_id'] = wf_crm_aval($existing, "$i:location_type_id", 1);
}
$params['is_primary'] = $i == 1 ? 1 : 0;
// Override that just in cases we know
if ($location == 'address' && $params['location_type_id'] == $is_primary_address_location_type) {
$params['is_primary'] = 1;
}
if ($location == 'email' && $params['location_type_id'] == $is_primary_email_location_type) {
$params['is_primary'] = 1;
}
if (isset($this->settings["civicrm_{$c}_contact_1_{$location}_is_primary"]) && $this->settings["civicrm_{$c}_contact_1_{$location}_is_primary"] == 0) {
unset($params['is_primary']);
}
}
$this->utils->wf_civicrm_api($location, 'create', $params);
}
}
}
}
/**
* Save groups and tags for a contact
* @param array $contact
* @param int $cid
* @param int $c
*/
private function saveGroupsAndTags($contact, $cid, $c) {
// Process groups & tags
foreach ($this->all_fields as $fid => $field) {
[$set, $type] = explode('_', $fid, 2);
if ($set == 'other') {
$field_name = 'civicrm_' . $c . '_contact_1_' . $fid;
if (!empty($contact['other'][1][$type]) || isset($this->enabled[$field_name])) {
$add = wf_crm_aval($contact, "other:1:$type", []);
// ToDo - $add should only contain the option(s) selected so unset everything else b/c addOrRemoveMultivaluedData is expecting that and we need this to handle TagSets - this is essentially a fail-safe
foreach ($this->getExposedOptions($field_name) as $k => $v) {
if (array_key_exists($k, $add) && $add[$k] === 0) {
unset($add[$k]);
}
}
$remove = empty($this->existing_contacts[$c]) ? [] : $this->getExposedOptions($field_name, $add);
$this->addOrRemoveMultivaluedData($field['table'], 'contact', $cid, $add, $remove);
}
}
}
}
/**
* Handle adding/removing multivalued data for a contact/activity/etc.
* Currently used only for groups and tags, but written with expansion in mind.
*
* @param $data_type
* 'group' or 'tag'
* @param $entity_type
* Parent entity: 'contact' etc.
* @param $id
* Entity id
* @param $add
* Groups/tags to add
* @param array $remove
* Groups/tags to remove
*/
private function addOrRemoveMultivaluedData($data_type, $entity_type, $id, $add, $remove = []) {
$confirmations_sent = $existing = $params = [];
$add = array_combine($add, $add);
static $mailing_lists = [];
switch ($data_type) {
case 'group':
$api = 'group_contact';
$params['method'] = substr(t('Webform'), 0, 8);
break;
case 'tag':
$api = 'EntityTag';
break;
default:
$api = $data_type;
}
if (!empty($add) || !empty($remove)) {
// Retrieve current records for this entity
if ($api == 'EntityTag') {
$params['entity_id'] = $id;
$params['entity_table'] = 'civicrm_' . $entity_type;
}
elseif ($api == 'group_contact' && $entity_type == 'contact') {
$params['contact_id'] = $id;
}
else {
$params['entity_id'] = $id;
$params['entity_type'] = 'civicrm_' . $entity_type;
}
$fetch = $this->utils->wf_civicrm_api($api, 'get', $params);
foreach (wf_crm_aval($fetch, 'values', []) as $i) {
$existing[] = $i[$data_type . '_id'];
unset($add[$i[$data_type . '_id']]);
}
foreach ($remove as $i => $name) {
if (!in_array($i, $existing)) {
unset($remove[$i]);
}
}
}
if (!empty($add)) {
// Prepare for sending subscription confirmations
if ($data_type == 'group' && !empty($this->settings['confirm_subscription'])) {
// Retrieve this contact's primary email address and perform error-checking
$result = $this->utils->wf_civicrm_api('email', 'get', ['contact_id' => $id, 'options' => ['sort' => 'is_primary DESC']]);
if (!empty($result['values'])) {
foreach ($result['values'] as $value) {
if (($value['is_primary'] || empty($email)) && strpos($value['email'], '@')) {
$email = $value['email'];
}
}
$mailer_params = [
'contact_id' => $id,
'email' => $email,
];
if (empty($mailing_lists)) {
$mailing_lists = $this->utils->wf_crm_apivalues('group', 'get', ['visibility' => 'Public Pages', 'group_type' => 2], 'title');
}
}
}
foreach ($add as $a) {
$params[$data_type . '_id'] = $mailer_params['group_id'] = $a;
if ($data_type == 'group' && isset($mailing_lists[$a]) && !empty($email)) {
$result = $this->utils->wf_civicrm_api('mailing_event_subscribe', 'create', $mailer_params);
if (empty($result['is_error'])) {
$confirmations_sent[] = Html::escape($mailing_lists[$a]);
}
else {
$this->utils->wf_civicrm_api($api, 'create', $params);
}
}
else {
$this->utils->wf_civicrm_api($api, 'create', $params);
}
}
if ($confirmations_sent) {
\Drupal::messenger()->addStatus(t('A message has been sent to %email to confirm subscription to :group.', ['%email' => $email, ':group' => '' . implode('' . t('and') . '', $confirmations_sent) . '']));
}
}
// Remove data from entity
foreach ($remove as $a => $name) {
$params[$data_type . '_id'] = $a;
if ($data_type == 'group') {
// group_contact.delete is deprecated and it just called create.
$this->utils->wf_civicrm_api($api, 'create', array_merge(['status' => 'Removed'], $params));
}
else {
$this->utils->wf_civicrm_api($api, 'delete', $params);
}
}
if (!empty($remove) && $data_type == 'group') {
$display_name = $this->utils->wf_civicrm_api('contact', 'get', ['contact_id' => $id, 'return.display_name' => 1]);
$display_name = wf_crm_aval($display_name, "values:$id:display_name", t('Contact'));
\Drupal::messenger()->addStatus(t('%contact has been removed from @group.', ['%contact' => $display_name, '@group' => '<em>' . implode('</em> ' . t('and') . ' <em>', $remove) . '</em>']));
}
}
/**
* Save relationships for a contact
*
* @param array $contact
* @param int $cid
* @param int $c
*/
private function saveRelationships($contact, $cid, $c) {
// Process relationships
foreach (wf_crm_aval($contact, 'relationship', []) as $n => $params) {
$relationship_type_id = (array) wf_crm_aval($params, 'relationship_type_id');
// Expire un-selected relationships.
$field_key = "civicrm_{$c}_contact_{$n}_relationship_relationship_type_id";
$remove = array_keys($this->getExposedOptions($field_key, (array) $params['relationship_type_id']));
if (!empty($remove)) {
$this->expireRelationship($remove, $cid, $this->ent['contact'][$n]['id']);
}
// Create new relationships.
if (!empty($relationship_type_id)) {
foreach ($relationship_type_id as $params['relationship_type_id']) {
$this->processRelationship($params, $cid, $this->ent['contact'][$n]['id']);
}
}
}
}
/**
* End relationship for a pair of contacts
*
* @param $type
* @param $cid1
* @param $cid2
*/
private function expireRelationship($removeTypes, $cid1, $cid2) {
foreach ($removeTypes as $type) {
$existing = $this->getRelationship([$type], $cid1, $cid2, TRUE);
if (empty($existing['id'])) {
continue;
}
$params = [
'id' => $existing['id'],
'end_date' => 'now',
'is_active' => 0,
];
$this->utils->wf_civicrm_api('relationship', 'create', $params);
}
}
/**
* Add/update relationship for a pair of contacts
*
* @param $params
* Params array for relationship api
* @param $cid1
* Contact id
* @param $cid2
* Contact id
*/
private function processRelationship($params, $cid1, $cid2) {
if (!empty($params['relationship_type_id']) && $cid2 && $cid1 != $cid2) {
[$type, $side] = explode('_', $params['relationship_type_id']);
$existing = $this->getRelationship([$params['relationship_type_id']], $cid1, $cid2);
$perm = wf_crm_aval($params, 'relationship_permission');
// Swap contacts if this is an inverse relationship
if ($side == 'b' || ($existing && $existing['contact_id_a'] != $cid1)) {
[$cid1, $cid2] = [$cid2, $cid1];
if ($perm == 1 || $perm == 2) {
$perm = $perm == 1 ? 2 : 1;
}
}
//initialise start_date for create action.
if (empty($existing) && !array_key_exists('start_date', $params)) {
$params['start_date'] = 'now';
}
$params += $existing;
$params['contact_id_a'] = $cid1;
$params['contact_id_b'] = $cid2;
$params['relationship_type_id'] = $type;
if ($perm) {
$params['is_permission_a_b'] = $params['is_permission_b_a'] = $perm == 3 ? 1 : 0;
if ($perm == 1 || $perm == 2) {
$params['is_permission_' . ($perm == 1 ? 'a_b' : 'b_a')] = 1;
}
}
unset($params['relationship_permission']);
$this->utils->wf_civicrm_api('relationship', 'create', $params);
}
}
/**
* Process event participation for a contact
* @param int $c
* @param int $cid
*/
private function processParticipants($c, $cid) {
static $registered_by_id = [];
$n = $this->data['participant_reg_type'] == 'separate' ? $c : 1;
if ($p = wf_crm_aval($this->data, "participant:$n:participant")) {
// Fetch existing participant records
$existing = $this->utils->wf_crm_apivalues('Participant', 'get', [
'return' => ["event_id"],
'contact_id' => $cid,
'is_test' => 0,
]);
$existing = array_column($existing, 'id', 'event_id');
foreach ($p as $e => $params) {
$remove = [];
$fid = "civicrm_{$c}_participant_{$e}_participant_event_id";
// Automatic status - de-selected events will be cancelled if 'disable_unregister' is not selected
if (empty($this->data['reg_options']['disable_unregister'])) {
if (empty($params['status_id'])) {
foreach ($this->getExposedOptions($fid) as $eid => $title) {
[$eid] = explode('-', $eid);
if (isset($existing[$eid])) {
$remove[$eid] = $title;
}
}
}
}
if (!empty($params['event_id'])) {
$params['contact_id'] = $cid;
if (empty($params['campaign_id']) || empty($this->all_fields['participant_campaign_id'])) {
unset($params['campaign_id']);
}
// Loop through event ids to support multi-valued form elements
$this->events = (array) $params['event_id'];
foreach ($this->events as $i => $id_and_type) {
if (!empty($id_and_type)) {
[$eid] = explode('-', $id_and_type);
$params['event_id'] = $eid;
unset($remove[$eid], $params['registered_by_id'], $params['id'], $params['source']);
// Is existing participant?
if (!empty($existing[$eid])) {
$params['id'] = $existing[$params['event_id']];
}
else {
if (isset($this->data['contact'][$c]['contact'][1]['source'])) {
$params['source'] = $this->data['contact'][$c]['contact'][1]['source'];
}
else {
$params['source'] = $this->settings['new_contact_source'];
}
if ($c > 1 && !empty($registered_by_id[$e][$i])) {
$params['registered_by_id'] = $registered_by_id[$e][$i];
}
}
// Automatic status
if (empty($params['status_id']) && empty($params['id'])) {
$params['status_id'] = 'Registered';
// Pending payment status
if ($this->contributionIsIncomplete && !empty($params['fee_amount'])) {
$params['status_id'] = $this->contributionIsPayLater ? 'Pending from pay later' : 'Pending from incomplete transaction';
}
}
// Do not update status of existing participant in "Automatic" mode
if (empty($params['status_id'])) {
unset($params['status_id']);
}
// Set the currency of the result to the currency type that was submitted.
if (isset($this->data['contribution'][$n]['currency'])) {
$params['fee_currency'] = $this->data['contribution'][$n]['currency'];
}
$result = $this->utils->wf_civicrm_api('participant', 'create', $params);
$this->ent['participant'][$n]['id'] = $result['id'];
// Update line-item
foreach ($this->line_items as &$item) {
if ($item['element'] == "civicrm_{$n}_participant_{$e}_participant_{$id_and_type}") {
if (empty($item['participant_id'])) {
$item['participant_id'] = $item['entity_id'] = $result['id'];
}
$item['participant_count'] = wf_crm_aval($item, 'participant_count', 0) + 1;
break;
}
}
// When registering contact 1, store id to apply to other contacts
if ($c == 1 && empty($this->data['reg_options']['disable_primary_participant'])) {
$registered_by_id[$e][$i] = $result['id'];
}
}
}
}
foreach ($remove as $eid => $title) {
$this->utils->wf_civicrm_api('participant', 'create', ['status_id' => "Cancelled", 'id' => $existing[$eid]]);
\Drupal::messenger()->addStatus(t('Registration cancelled for @event', ['@event' => $title]));
}
}
}
}
/**
* Process memberships for a contact
* Called during webform submission
* @param int $c
* @param int $cid
*/
private function processMemberships($c, $cid) {
static $types;
if (!isset($types)) {
$types = $this->utils->wf_crm_apivalues('membership_type', 'get');
}
$existing = $this->findMemberships($cid);
foreach (wf_crm_aval($this->data, "membership:$c:membership", []) as $n => $params) {
$membershipStatus = "";
$membershipEndDate = "";
$is_active = FALSE;
if (empty($params['membership_type_id'])) {
continue;
}
if (empty($params['status_id'])) {
$params['status_override_end_date'] = '';
}
// Search for existing membership to renew - must belong to same domain and organization
// But not necessarily the same membership type to allow for upsell
if (!empty($params['num_terms'])) {
$type = $types[$params['membership_type_id']];
foreach ($existing as $mem) {
$existing_type = $types[$mem['membership_type_id']];
if ($type['domain_id'] == $existing_type['domain_id'] && $type['member_of_contact_id'] == $existing_type['member_of_contact_id']) {
$params['id'] = $mem['id'];
// If we have an exact match, look no further
if ($mem['membership_type_id'] == $params['membership_type_id']) {
$is_active = $mem['is_active'];
$membershipStatus = $mem['status'];
$membershipEndDate = $mem['end_date'];
break;
}
}
}
}
if (empty($params['id'])) {
if (isset($this->data['contact'][$c]['contact'][1]['source'])) {
$params['source'] = $this->data['contact'][$c]['contact'][1]['source'];
}
else {
$params['source'] = $this->settings['new_contact_source'];
}
}
// Automatic status
if (empty($params['status_id'])) {
unset($params['status_id']);
// Pending payment status
if ($this->contributionIsIncomplete && $this->getMembershipTypeField($params['membership_type_id'], 'minimum_fee')) {
if ($is_active == FALSE) {
$params['status_id'] = 'Pending';
} else {
$params['status_id'] = $membershipStatus;
$params['end_date'] = $membershipEndDate;
}
}
}
// Override status
else {
$params['is_override'] = 1;
}
$params['contact_id'] = $cid;
// The api won't let us manually set status without this weird param
$params['skipStatusCal'] = !empty($params['status_id']);
if (isset($this->ent['contribution_recur'][1]['id'])) {
$params['contribution_recur_id'] = $this->ent['contribution_recur'][1]['id'];
}
$result = $this->utils->wf_civicrm_api('membership', 'create', $params);
if (!empty($result['id'])) {
// Issue #2516924 If existing membership create renewal activity
if (!empty($params['id'])) {
$membership = $result['values'][$result['id']];
$actParams = [
'source_contact_id' => $cid,
'activity_type_id' => 'Membership Renewal',
'target_id' => $cid,
'source_record_id' => $result['id'],
];
$memType = $this->utils->wf_civicrm_api('MembershipType', 'getsingle', ['id' => $membership['membership_type_id']]);
$memStatus = $this->utils->wf_civicrm_api('MembershipStatus', 'getsingle', ['id' => $membership['status_id']]);
$actParams['subject'] = ts("%1 - Status: %2", [1 => $memType['name'], 2 => $memStatus['label']]);
$this->utils->wf_civicrm_api('Activity', 'create', $actParams);
}
foreach ($this->line_items as &$item) {
if ($item['element'] == "civicrm_{$c}_membership_{$n}") {
$item['membership_id'] = $result['id'];
break;
}
}
}
}
}
/**
* Process shared addresses
*/
private function processSharedAddresses() {
foreach ($this->shared_address as $cid => $shared) {
foreach ($shared as $i => $addr) {
if (!empty($this->ent['contact'][$addr['mc']]['id'])) {
$masters = $this->utils->wf_civicrm_api('address', 'get', ['contact_id' => $this->ent['contact'][$addr['mc']]['id'], 'options' => ['sort' => 'is_primary DESC']]);
if (!empty($masters['values'])) {
$masters = array_values($masters['values']);
// Pick the address with the same location type; default to primary.
$params = $masters[0];
foreach ($masters as $m) {
if ($m['location_type_id'] == $addr['loc']) {
$params = $m;
break;
}
}
$params['master_id'] = $params['id'];
$params['id'] = $addr['id'];
$params['contact_id'] = $cid;
$params['is_primary'] = $addr['pri'];
$this->utils->wf_civicrm_api('address', 'create', $params);
}
}
}
}
}
/**
* Save case data
*/
private function processCases() {
foreach (wf_crm_aval($this->data, 'case', []) as $n => $data) {
if (is_array($data) && !empty($data['case'][1]['client_id'])) {
$params = $data['case'][1];
//Check if webform is set to update the existing case on contact.
if (!empty($this->data['case'][$n]['existing_case_status']) && empty($this->ent['case'][$n]['id']) && !empty($this->ent['contact'][$n]['id'])) {
$existingCase = $this->findCaseForContact($this->ent['contact'][$n]['id'], [
'case_type_id' => wf_crm_aval($this->data['case'][$n], 'case:1:case_type_id'),
'status_id' => $this->data['case'][$n]['existing_case_status']
]);
if (!empty($existingCase['id'])) {
$this->ent['case'][$n]['id'] = $existingCase['id'];
}
}
// Set some defaults in create mode
// Create duplicate case based on existing case if 'duplicate_case' checkbox is checked
if (empty($this->ent['case'][$n]['id']) || !empty($this->data['case'][$n]['duplicate_case'])) {
if (empty($params['case_type_id'])) {
// Abort if no case type.
continue;
}
// Automatic status... for lack of anything fancier just pick the first option ("Ongoing" on a standard install)
if (empty($params['status_id'])) {
$options = $this->utils->wf_crm_apivalues('case', 'getoptions', ['field' => 'status_id']);
$params['status_id'] = current(array_keys($options));
}
if (empty($params['subject'])) {
$params['subject'] = Html::escape($this->node->get('title'));
}
// Automatic creator_id - default to current user or contact 1
if (empty($data['case'][1]['creator_id'])) {
if (\Drupal::currentUser()->isAuthenticated()) {
$params['creator_id'] = $this->utils->wf_crm_user_cid();
}
elseif (!empty($this->ent['contact'][1]['id'])) {
$params['creator_id'] = $this->ent['contact'][1]['id'];
}
else {
// Abort if no creator available
continue;
}
}
}
// Update mode
else {
$params['id'] = $this->ent['case'][$n]['id'];
// These params aren't allowed in update mode
unset($params['creator_id']);
}
// If orig_contact_id is missing and there's more than one Contact
// on the Case, set orig_contact_id to the current ID. Otherwise
// we get a Case.create error "Case is linked with more than one
// contact id".
if (empty($params['orig_contact_id']) && !empty($params['id'])) {
$caseContactCount = $this->utils->wf_civicrm_api('CaseContact', 'getcount', ['case_id' => $params['id']]);
if ($caseContactCount > 1 && !empty($params['client_id'])) {
$params['orig_contact_id'] = current((array) $params['client_id']);
}
}
// Allow "automatic" status to pass-thru
if (empty($params['status_id'])) {
unset($params['status_id']);
}
$result = $this->utils->wf_civicrm_api('case', 'create', $params);
// Final processing if save was successful
if (!empty($result['id'])) {
// handle case tags
$this->handleEntityTags('case', $result['id'], $n, $params);
// Store id
$this->ent['case'][$n]['id'] = $result['id'];
$relationshipStartDate = date('Ymd');
// Save case roles
foreach ($params as $param => $val) {
if ($val && strpos($param, 'role_') === 0) {
foreach ((array) $params['client_id'] as $client) {
foreach ((array) $val as $contactB) {
$relationshipParams = [
'relationship_type_id' => substr($param, 5),
'contact_id_a' => $client,
'contact_id_b' => $contactB,
'case_id' => $result['id'],
'is_active' => TRUE,
];
// We can't create a duplicate relationship so check if active relationship exists first.
$existingRelationships = $this->utils->wf_civicrm_api('relationship', 'get', $relationshipParams);
if (!empty($existingRelationships['count'])) {
// Update the existing active relationship (case role)
$relationshipParams['id'] = reset($existingRelationships['values'])['id'];
}
else {
// We didn't used to set start_date - now we do when creating new relationships (case roles)
$relationshipParams['start_date'] = $relationshipStartDate;
}
$this->utils->wf_civicrm_api('relationship', 'create', $relationshipParams);
}
}
}
}
}
}
}
}
/**
* Save activity data
*/
private function processActivities() {
$activities = wf_crm_aval($this->data, 'activity', []);
foreach ($activities as $n => $data) {
if (is_array($data)) {
$params = $data['activity'][1];
// Create mode
if (empty($this->ent['activity'][$n]['id'])) {
// Skip if no activity type
if (empty($params['activity_type_id'])) {
continue;
}
// Automatic status based on whether activity_date_time is in the future
if (empty($params['status_id'])) {
$params['status_id'] = strtotime(wf_crm_aval($params, 'activity_date_time', 'now')) > time() ? 'Scheduled' : 'Completed';
}
// Automatic source_contact_id - default to current user or contact 1
if (empty($data['activity'][1]['source_contact_id'])) {
if (\Drupal::currentUser()->isAuthenticated()) {
$params['source_contact_id'] = $this->utils->wf_crm_user_cid();
}
elseif (!empty($this->ent['contact'][1]['id'])) {
$params['source_contact_id'] = $this->ent['contact'][1]['id'];
}
}
// Format details as html
$this->formatSubmissionDetails($params, $n);
}
// Update mode
else {
$params['id'] = $this->ent['activity'][$n]['id'];
// Update details when user has selected he wants to update the details
if (!empty($data['details']['update_existing'])) {
// Format details as html
$this->formatSubmissionDetails($params, $n);
}
}
// Allow "automatic" values to pass-thru if empty
foreach ($params as $field => $value) {
if ((isset($this->all_fields["activity_$field"]['empty_option']) || isset($this->all_fields["activity_$field"]['exposed_empty_option'])) && !$value) {
unset($params[$field]);
}
}
// Handle survey data
if (!empty($params['survey_id'])) {
$params['source_record_id'] = $params['survey_id'];
// Set default subject
if (empty($params['id']) && empty($params['subject'])) {
$survey = $this->utils->wf_civicrm_api('survey', 'getsingle', ['id' => $params['survey_id'], 'return' => 'title']);
$params['subject'] = wf_crm_aval($survey, 'title', '');
}
}
// File on case
if (!empty($data['case_type_id'])) {
// Webform case
if ($data['case_type_id'][0] === '#') {
$case_num = substr($data['case_type_id'], 1);
if (!empty($this->ent['case'][$case_num]['id'])) {
$params['case_id'] = $this->ent['case'][$case_num]['id'];
}
}
// Search for case by criteria
else {
$case_contact = $this->ent['contact'][$data['case_contact_id']]['id'];
if ($case_contact) {
// Proceed only if this activity is not already filed on a case
if (empty($params['id']) || !$this->utils->wf_crm_apivalues('case', 'get', ['activity_id' => $params['id']])) {
$case = $this->findCaseForContact($case_contact, [
'status_id' => $data['case_status_id'],
'case_type_id' => $data['case_type_id'],
]);
if ($case) {
$params['case_id'] = $case['id'];
}
}
}
}
}
$activity = $this->utils->wf_civicrm_api('activity', 'create', $params);
// Final processing if save was successful
if (!empty($activity['id'])) {
// handle activity tags
$this->handleEntityTags('activity', $activity['id'], $n, $params);
// Store id
$this->ent['activity'][$n]['id'] = $activity['id'];
// Save attachments
if (isset($data['activityupload'])) {
$this->processAttachments('activity', $n, $activity['id'], empty($params['id']));
}
if (!empty($params['assignee_contact_id'])) {
if ($this->utils->wf_crm_get_civi_setting('activity_assignee_notification')
&& !in_array($params['activity_type_id'], $this->utils->wf_crm_get_civi_setting('do_not_notify_assignees_for'))
) {
// Send email to assignees. TODO: Move to CiviCRM API?
$assignees = $this->utils->wf_crm_apivalues('contact', 'get', [
'id' => ['IN' => (array) $params['assignee_contact_id']],
]);
$mail = [];
foreach ($assignees as $assignee) {
if (!empty($assignee['email'])) {
$mail[$assignee['email']] = $assignee;
}
}
if ($mail) {
// Include attachments while sending a copy of activity.
$attachments = \CRM_Core_BAO_File::getEntityFile('civicrm_activity', $activity['id']);
\CRM_Case_BAO_Case::sendActivityCopy(NULL, $activity['id'], $mail, $attachments, NULL);
}
}
}
}
}
}
}
/**
* Handle adding/updating tags for entities (cases, activity)
*
* @param $entityType
* @param $entityId
* @param $n
* @param $params
*/
public function handleEntityTags($entityType, $entityId, $n, $params) {
foreach ($this->all_fields as $fid => $field) {
[$set, $type] = explode('_', $fid, 2);
if ($set == $entityType && isset($field['table']) && $field['table'] == 'tag') {
$field_name = 'civicrm_' . $n . '_' . $entityType. '_1_' . $fid;
if ((isset($params['tag']) || isset($this->enabled[$field_name])) && isset($this->data[$entityType][$n])) {
$add = wf_crm_aval($this->data[$entityType][$n], $entityType . ":1:$type", []);
$remove = $this->getExposedOptions($field_name, $add);
$this->addOrRemoveMultivaluedData('tag', $entityType, $entityId, $add, $remove);
}
}
}
}
/**
* Process the submission into the details of the activity.
*/
private function formatSubmissionDetails(&$params, $activity_number) {
// Format details as html
$params['details'] = nl2br(wf_crm_aval($params, 'details', ''));
// Add webform results to details
if (!empty($this->data['activity'][$activity_number]['details']['entire_result'])) {
$view_builder = \Drupal::entityTypeManager()->getViewBuilder('webform_submission');
$submission = $view_builder->view($this->submission);
$params['details'] .= \Drupal::service('renderer')->renderPlain($submission);
}
if (!empty($this->data['activity'][$activity_number]['details']['view_link'])) {
$params['details'] .= '<p>' . $this->submission->toLink(t('View Webform Submission'), 'canonical', [
'absolute' => TRUE,
])->toString() . '</p>' . \Drupal\Core\Link::fromTextAndUrl('View Webform Submission', $this->submission->getTokenUrl('view'))->toString();
}
if (!empty($this->data['activity'][$activity_number]['details']['view_link_secure'])) {
$params['details'] .= '<p>' . \Drupal\Core\Link::fromTextAndUrl('View Webform Submission', $this->submission->getTokenUrl('view'))->toString() . '</p>';
}
if (!empty($this->data['activity'][$activity_number]['details']['edit_link'])) {
$params['details'] .= '<p>' . $this->submission->toLink(t('Edit Submission'), 'edit-form', [
'absolute' => TRUE,
])->toString() . '</p>';
}
}
/**
* Save grants
*/
private function processGrants() {
foreach (wf_crm_aval($this->data, 'grant', []) as $n => $data) {
if (is_array($data) && !empty($data['grant'][1]['contact_id'])) {
$params = $data['grant'][1];
// Set some defaults in create mode
if (empty($this->ent['grant'][$n]['id'])) {
// Automatic status... for lack of anything fancier just pick the first option ("Submitted" on a standard install)
if (empty($params['status_id'])) {
$options = $this->utils->wf_crm_apivalues('grant', 'getoptions', ['field' => 'status_id']);
$params['status_id'] = current(array_keys($options));
}
if (empty($params['application_received_date'])) {
$params['application_received_date'] = 'now';
}
if (empty($params['grant_report_received'])) {
$params['grant_report_received'] = '0';
}
}
// Update mode
else {
$params['id'] = $this->ent['grant'][$n]['id'];
}
// Allow "automatic" status to pass-thru
if (empty($params['status_id'])) {
unset($params['status_id']);
}
$result = $this->utils->wf_civicrm_api('grant', 'create', $params);
// Final processing if save was successful
if (!empty($result['id'])) {
// Store id
$this->ent['grant'][$n]['id'] = $result['id'];
// Save attachments
if (isset($data['grantupload'])) {
$this->processAttachments('grant', $n, $result['id'], empty($params['id']));
}
}
}
}
}
/**
* Calculate line-items for this webform submission
*/
private function tallyLineItems() {
$submittedFormValues = $this->form_state->getUserInput();
// Contribution
$fid = 'civicrm_1_contribution_1_contribution_total_amount';
if (isset($this->enabled[$fid]) || $this->getData($fid) > 0) {
$this->line_items[] = [
'qty' => 1,
'unit_price' => $this->getData($fid),
'financial_type_id' => wf_crm_aval($this->data, 'contribution:1:contribution:1:financial_type_id'),
'label' => wf_crm_aval($this->node->getElementsDecodedAndFlattened(), $this->enabled[$fid] . ':#title', t('Contribution')),
'element' => 'civicrm_1_contribution_1',
'entity_table' => 'civicrm_contribution',
];
}
// LineItems
$fid = 'civicrm_1_lineitem_1_contribution_line_total';
if (isset($this->enabled[$fid])) {
foreach ($this->data['lineitem'][1]['contribution'] as $n => $lineitem) {
$fid = "civicrm_1_lineitem_{$n}_contribution_line_total";
if (!isset($submittedFormValues[$fid]) || $this->getData($fid) != 0) {
$this->line_items[] = [
'qty' => 1,
'unit_price' => $lineitem['line_total'],
'financial_type_id' => $lineitem['financial_type_id'],
'label' => wf_crm_aval($this->node->getElementsDecodedAndFlattened(), $this->enabled[$fid] . ':#title', t('Line Item')),
'element' => "civicrm_1_lineitem_{$n}",
'entity_table' => 'civicrm_contribution',
];
}
}
}
// Membership
foreach (wf_crm_aval($this->data, 'membership', []) as $c => $memberships) {
if (isset($this->existing_contacts[$c]) && !empty($memberships['number_of_membership'])) {
foreach ($memberships['membership'] as $n => $membership_item) {
if (!empty($membership_item['membership_type_id'])) {
$type = $membership_item['membership_type_id'];
$price = $this->getMembershipTypeField($type, 'minimum_fee');
// if number of terms is set, regard membership fee field as price per term
// if you choose to set dates manually while membership fee field is enabled, take the membership fee as total cost of this membership
if (isset($membership_item['fee_amount'])) {
$price = $membership_item['fee_amount'];
if (empty($membership_item['num_terms'])) {
$membership_item['num_terms'] = 1;
}
}
$membership_financialtype = $this->getMembershipTypeField($type, 'financial_type_id');
if (isset($membership_item['financial_type_id']) && $membership_item['financial_type_id'] !== 0 ) {
$membership_financialtype = $membership_item['financial_type_id'];
};
if ($price) {
$member_name = NULL;
if (!empty($this->data['contact'][$c]['contact'][$n])) {
$member_contact = $this->data['contact'][$c]['contact'][$n];
if (!empty($member_contact['first_name']) && !empty($member_contact['last_name'])) {
$member_name = "{$member_contact['first_name']} {$member_contact['last_name']}";
}
}
// If member name is not entered on the form, retrieve it from civicrm.
if (empty($member_name)) {
$member_name = $this->utils->wf_crm_display_name($this->existing_contacts[$c]);
}
$this->line_items[] = [
'qty' => $membership_item['num_terms'],
'unit_price' => $price,
'financial_type_id' => $membership_financialtype,
'label' => $this->getMembershipTypeField($type, 'name') . ": " . $member_name,
'element' => "civicrm_{$c}_membership_{$n}",
'entity_table' => 'civicrm_membership',
];
}
}
}
}
}
// Calculate totals
$this->totalContribution = 0;
$taxRates = \CRM_Core_PseudoConstant::getTaxRates();
foreach ($this->line_items as &$line_item) {
// Sales Tax integration
if (!empty($line_item['financial_type_id'])) {
$itemTaxRate = isset($taxRates[$line_item['financial_type_id']]) ? $taxRates[$line_item['financial_type_id']] : NULL;
}
else {
$itemTaxRate = $this->tax_rate;
}
if ($itemTaxRate !== NULL) {
$line_item['tax_rate'] = $itemTaxRate;
$line_item['line_total'] = $line_item['unit_price'] * (int) $line_item['qty'];
$line_item['tax_amount'] = ($itemTaxRate / 100) * $line_item['line_total'];
$this->totalContribution += ($line_item['unit_price'] * (int) $line_item['qty']) + $line_item['tax_amount'];
}
else {
$line_item['line_total'] = $line_item['unit_price'] * (int) $line_item['qty'];
$this->totalContribution += $line_item['line_total'];
}
}
return round($this->totalContribution, 2);
}
/**
* Are billing fields exposed to this webform page?
* @return bool
*/
private function isPaymentPage() {
// @todo see if this needs to be more dynamic.
return $this->form_state->get('current_page') === 'contribution_pagebreak';
}
/**
* @return bool
*/
private function isLivePaymentProcessor() {
if ($this->payment_processor) {
if ($this->payment_processor['billing_mode'] == self::BILLING_MODE_LIVE) {
return TRUE;
}
// In mixed mode (where there is e.g. a PayPal button + credit card fields) the cc field will contain a placeholder if the button was clicked
if ($this->payment_processor['billing_mode'] == self::BILLING_MODE_MIXED && wf_crm_aval($_POST, 'credit_card_number') != 'express') {
return TRUE;
}
}
return FALSE;
}
/**
* Normalize and validate billing input
* @return bool
*/
private function validateBillingFields() {
$valid = TRUE;
$params = $card_errors = [];
$payment_processor_id = NestedArray::getValue($this->data, ['contribution', '1', 'contribution', '1', 'payment_processor_id']);
$processor = \Civi\Payment\System::singleton()->getById($payment_processor_id);
$billingAddressFieldsMetadata = [];
if (method_exists($processor, 'getBillingAddressFieldsMetadata')) {
// getBillingAddressFieldsMetadata did not exist before 4.7
$billingAddressFieldsMetadata = $processor->getBillingAddressFieldsMetadata();
}
$fields = \CRM_Utils_Array::crmArrayMerge($processor->getPaymentFormFieldsMetadata(), $billingAddressFieldsMetadata);
$request = \Drupal::request();
$form_values = array_merge($request->request->all(), $this->form_state->getValues());
foreach ($form_values as $field => $value) {
if (empty($value) && isset($fields[$field]) && $fields[$field]['is_required'] !== FALSE) {
$this->form_state->setErrorByName($field, t(':name field is required.', [':name' => $fields[$field]['title']]));
$valid = FALSE;
}
if (!empty($value) && (array_key_exists($field, $fields) || strpos($field, 'billing_address_') !== false)) {
$name = str_replace('civicrm_1_contribution_1_contribution_billing_address_', '', $field);
$params[$name] = $value;
foreach (["billing_{$name}", "billing_{$name}-5"] as $billingName) {
if (isset($fields[$billingName])) {
$params[$billingName] = $value;
}
}
}
}
// 4.6 compatibility - some processors (eg. authorizeNet / Paypal require that "year" and "month" are set in params.
if (isset($params['credit_card_exp_date'])) {
$params['year'] = \CRM_Core_Payment_Form::getCreditCardExpirationYear($params);
$params['month'] = \CRM_Core_Payment_Form::getCreditCardExpirationMonth($params);
}
// Validate billing details
if (!empty($this->data['billing']['number_number_of_billing'])) {
\CRM_Core_Payment_Form::validatePaymentInstrument($payment_processor_id, $params, $card_errors, NULL);
}
foreach ($card_errors as $field => $msg) {
$this->form_state->setErrorByName($field, $msg);
$valid = FALSE;
}
// Email
$c = $this->getContributionContactIndex();
for ($i = 1; $i <= $this->data['contact'][$c]['number_of_email']; ++$i) {
if (!empty($this->crmValues["civicrm_{$c}_contact_{$i}_email_email"])) {
$params['email'] = $this->crmValues["civicrm_{$c}_contact_{$i}_email_email"];
break;
}
}
if (empty($params['email']) AND !$processor->supports('NoEmailProvided')) {
$this->form_state->setErrorByName('billing_email', ts('An email address is required to complete this transaction.'));
$valid = FALSE;
}
if ($valid) {
$this->billing_params = $params;
}
// Since billing fields are not "real" form fields they get cleared if the page reloads.
// We add a bit of js to fix this annoyance.
$this->form['#attached']['drupalSettings']['webform_civicrm']['billingSubmission'] = $params;
return $valid;
}
/**
* Return index value of contribution contact.
*/
private function getContributionContactIndex() {
return wf_crm_aval($this->settings, "data:contribution:1:contribution:1:contact_id") ?? wf_crm_aval($this->submissionValue("civicrm_1_contribution_1_contribution_contact_id"), 0) ?? 1;
}
/**
* Create contact 1 if not already existing (required by contribution.transact)
* @return int
*/
private function createBillingContact() {
$i = $this->getContributionContactIndex();
$cid = wf_crm_aval($this->existing_contacts, $i);
if (!$cid) {
$contact = $this->data['contact'][$i];
// Only use middle name from billing if we are using the rest of the billing name as well
if (empty($contact['contact'][1]['first_name']) && !empty($this->billing_params['middle_name'])) {
$contact['contact'][1]['middle_name'] = $this->billing_params['middle_name'];
}
$contact['contact'][1] += [
'first_name' => $this->billing_params['first_name'] ?? NULL,
'last_name' => $this->billing_params['last_name'] ?? NULL,
];
$cid = $this->findDuplicateContact($contact);
}
$address = [
'street_address' => $this->billing_params['street_address'] ?? NULL,
'city' => $this->billing_params['city'] ?? NULL,
'country_id' => $this->billing_params['country_id'] ?? NULL,
'state_province_id' => wf_crm_aval($this->billing_params, 'state_province_id'),
'postal_code' => $this->billing_params['postal_code'] ?? NULL,
'location_type_id' => 'Billing',
];
$email = [
'email' => $this->billing_params['email'],
'location_type_id' => 'Billing',
];
if (!$cid) {
// Current employer must wait for ContactRef ids to be filled
unset($contact['contact'][1]['employer_id']);
$cid = $this->createContact($contact);
}
else {
foreach (['address', 'email'] as $loc) {
$result = $this->utils->wf_civicrm_api($loc, 'get', [
'contact_id' => $cid,
'location_type_id' => 'Billing',
]);
// Use first id if we have any results
if (!empty($result['values'])) {
$ids = array_keys($result['values']);
${$loc}['id'] = $ids[0];
}
}
}
if ($cid) {
$address['contact_id'] = $email['contact_id'] = $this->ent['contact'][$i]['id'] = $cid;
// Don't create a blank billing address.
if ($address['street_address'] || $address['city'] || $address['country_id'] || $address['state_province_id'] || $address['postal_code']) {
$this->billing_params['billing_address_id'] = $this->utils->wf_civicrm_api('address', 'create', $address)['id'] ?? NULL;
}
if ($email['email'] ?? FALSE) {
$this->billing_params['billing_email_id'] = $this->utils->wf_civicrm_api('email', 'create', $email)['id'] ?? NULL;
}
}
return $cid;
}
/**
* Execute payment processor transaction
* This happens during form validation and sets a form error if unsuccessful
*/
private function submitLivePayment() {
$contributionParams = $this->contributionParams();
// Only if #installments = 1, do we process a single transaction/contribution. #installments = 0 (or not set) in CiviCRM Core means open ended commitment;
$numInstallments = wf_crm_aval($contributionParams, 'installments', NULL, TRUE);
$frequencyInterval = wf_crm_aval($contributionParams, 'frequency_unit');
if ($numInstallments != 1 && !empty($frequencyInterval)) {
$result = $this->contributionRecur($contributionParams);
}
else {
// Not a recur payment, remove any recur fields if present.
unset($contributionParams['frequency_unit'], $contributionParams['frequency_interval'], $contributionParams['is_recur']);
$result = $this->utils->wf_civicrm_api('contribution', 'transact', $contributionParams);
}
if (empty($result['id'])) {
if (!empty($result['error_message'])) {
$this->form_state->setErrorByName('contribution', $result['error_message']);
}
else {
$this->form_state->setErrorByName('contribution', t('Transaction failed. Please verify all billing fields are correct.'));
}
return;
}
$this->ent['contribution'][1]['id'] = $result['id'];
}
/**
*
* Generate recurring contribution and transact the first one
*/
private function contributionRecur($contributionParams, $paymentType = 'live') {
$numInstallments = wf_crm_aval($contributionParams, 'installments', NULL, TRUE);
// if ($installments === 0) or $installments is not defined we treat as an open-ended recur
$contributionFirstAmount = $contributionRecurAmount = $contributionParams['total_amount'];
$salesTaxFirstAmount = wf_crm_aval($contributionParams, 'tax_amount', NULL, TRUE);
$nondeductibleFirstAmount = wf_crm_aval($contributionParams, 'non_deductible_amount', NULL, TRUE);
if ($numInstallments > 0) {
// DRU-2862396 - we must ensure to only present 2 decimals to CiviCRM:
$contributionRecurAmount = round(($contributionParams['total_amount'] / $numInstallments), 2, PHP_ROUND_HALF_UP);
$contributionFirstAmount = $contributionRecurAmount;
$salesTaxFirstAmount = round(($salesTaxFirstAmount / $numInstallments), 2, PHP_ROUND_HALF_UP);
$nondeductibleFirstAmount = round(($nondeductibleFirstAmount / $numInstallments), 2, PHP_ROUND_HALF_UP);
// Calculate the line_items for the first contribution:
// At this point line_items are set to the full (non-installment) amounts.
foreach ($this->line_items as $key => $k) {
$this->line_items[$key]['unit_price'] = round(($k['unit_price'] / $numInstallments), 2, PHP_ROUND_HALF_UP);
$this->line_items[$key]['line_total'] = round(($k['line_total'] / $numInstallments), 2, PHP_ROUND_HALF_UP);
$this->line_items[$key]['tax_amount'] = round(($k['tax_amount'] / $numInstallments), 2, PHP_ROUND_HALF_UP);
}
}
// Create Params for Creating the Recurring Contribution Series and Create it
$contributionRecurParams = [
'contact_id' => $contributionParams['contact_id'],
'frequency_interval' => wf_crm_aval($contributionParams, 'frequency_interval', 1),
'frequency_unit' => wf_crm_aval($contributionParams, 'frequency_unit', 'month'),
'installments' => $numInstallments,
'amount' => $contributionRecurAmount,
'contribution_status_id' => 'In Progress',
'currency' => $contributionParams['currency'],
'payment_processor_id' => $contributionParams['payment_processor_id'],
'financial_type_id' => $contributionParams['financial_type_id'],
];
if (empty($contributionParams['payment_processor_id'])) {
$contributionRecurParams['payment_processor_id'] = 'null';
}
$resultRecur = $this->utils->wf_civicrm_api('ContributionRecur', 'create', $contributionRecurParams);
$this->ent['contribution_recur'][1]['id'] = $resultRecur['id'];
// Run the Transaction - and Create the Contribution Record - relay Recurring Series information in addition to the already existing Params [and re-key where needed]; at times two keys are required
$contributionParams['total_amount'] = $contributionFirstAmount;
$contributionParams['tax_amount'] = $salesTaxFirstAmount;
$contributionParams['non_deductible_amount'] = $nondeductibleFirstAmount;
$additionalParams = [
'contribution_recur_id' => $resultRecur['id'],
'contributionRecurID' => $resultRecur['id'],
'is_recur' => 1,
'contactID' => $contributionParams['contact_id'],
'frequency_interval' => wf_crm_aval($contributionParams, 'frequency_interval', 1),
'frequency_unit' => wf_crm_aval($contributionParams, 'frequency_unit', 'month'),
];
$APIAction = 'transact';
if (in_array($paymentType, ['deferred', 'ipn'], TRUE)) {
$APIAction = 'create';
}
$result = $this->utils->wf_civicrm_api('contribution', $APIAction, $contributionParams + $additionalParams);
// If transaction was successful - Update the Recurring Series - currency must be resubmitted or else it will re-default to USD; invoice_id is that of the initiating Contribution
if (empty($result['error_message'])) {
$recurParams = [
'id' => $resultRecur['id'],
'currency' => $contributionParams['currency'],
'next_sched_contribution_date' => date("Y-m-d H:i:s", strtotime('+' . $contributionRecurParams['frequency_interval'] . ' ' . $contributionRecurParams['frequency_unit'])),
];
$recurParams['invoice_id'] = $result['values'][$result['id']]['invoice_id'];
$recurParams['payment_instrument_id'] = $result['values'][$result['id']]['payment_instrument_id'];
$this->utils->wf_civicrm_api('ContributionRecur', 'create', $recurParams);
}
return $result;
}
/**
* Create Pending (Pay Later) Contribution
*/
private function createDeferredPayment() {
$this->contributionIsIncomplete = TRUE;
$this->contributionIsPayLater = FALSE;
$paymentProcessorID = NestedArray::getValue($this->data, ['contribution', '1', 'contribution', '1', 'payment_processor_id']);
if(empty($paymentProcessorID)) {
$this->contributionIsPayLater = TRUE;
}
else {
$paymentProcessorClassName = civicrm_api3('PaymentProcessor', 'getvalue', [
'return' => 'class_name',
'id' => $paymentProcessorID,
]);
if ($paymentProcessorClassName === 'Payment_Manual') {
$this->contributionIsPayLater = TRUE;
}
}
$params = $this->contributionParams();
$params['contribution_status_id'] = 'Pending';
$params['is_pay_later'] = $this->contributionIsPayLater;
$numInstallments = wf_crm_aval($params, 'installments', NULL, TRUE);
$frequencyInterval = wf_crm_aval($params, 'frequency_unit');
if ($numInstallments != 1 && !empty($frequencyInterval)) {
if ($this->contributionIsPayLater) {
$result = $this->contributionRecur($params, 'deferred');
}
elseif (empty($this->isLivePaymentProcessor())) {
$result = $this->contributionRecur($params, 'ipn');
}
}
if (empty($result['id'])) {
$result = $this->utils->wf_civicrm_api('contribution', 'create', $params);
}
$this->ent['contribution'][1]['id'] = $result['id'];
}
/**
* Call IPN payment processor to redirect to payment site
*/
private function submitIPNPayment() {
$params = $this->data['contribution'][1]['contribution'][1];
$processor_type = $this->utils->wf_civicrm_api('payment_processor', 'getsingle', ['id' => $params['payment_processor_id']]);
$paymentProcessor = \Civi\Payment\System::singleton()->getById($params['payment_processor_id']);
// Include billing fields with keys expected by payment processor.
$billingAddressFieldsMetadata = [];
if (method_exists($paymentProcessor, 'getBillingAddressFieldsMetadata')) {
$billingAddressFieldsMetadata = $paymentProcessor->getBillingAddressFieldsMetadata();
foreach ($params as $key => $value) {
if (strpos($key, 'billing_address_') !== false) {
$name = str_replace('billing_address_', '', $key);
foreach (["billing_{$name}", "billing_{$name}-5"] as $billingName) {
if (isset($billingAddressFieldsMetadata[$billingName])) {
$params[$billingName] = $value;
}
}
}
}
}
// Ideally we would pass the correct id for the test processor through but that seems not to be the
// case so load it here.
if (!empty($params['is_test'])) {
$paymentProcessor = \Civi\Payment\System::singleton()->getByName($processor_type['name'], TRUE);
}
// Add contact details to params (most processors want a first_name and last_name)
$i = $this->getContributionContactIndex();
$contact = $this->utils->wf_civicrm_api('contact', 'getsingle', ['id' => $this->ent['contact'][$i]['id']]);
$params += $contact;
// contact provides 'country' and 'country_id', but doPayment using PropertyBag expects 'billingCountry' with an iso_code
$countryName = $params['country'] ?? NULL;
$countryId = $params['country_id'] ?? NULL;
// providing country name throws deprecation warnings,
// which break the transaction so remove it
unset($params['country']);
// country id seems more reliable, so use that first
if ($countryId) {
$params['billingCountry'] = $this->utils->wf_civicrm_api4('Country', 'get', [
'select' => ['iso_code'],
'where' => [['id', '=', $countryId]]
])->first()['iso_code'] ?? '';
}
elseif ($countryName) {
$params['billingCountry'] = $this->utils->wf_civicrm_api4('Country', 'get', [
'select' => ['iso_code'],
'where' => [['name', '=', $countryName]]
])->first()['iso_code'] ?? '';
}
$params['contributionID'] = $params['id'] = $this->ent['contribution'][1]['id'];
if (!empty($this->ent['contribution_recur'][1]['id'])) {
$params['is_recur'] = TRUE;
$params['contributionRecurID'] = $this->ent['contribution_recur'][1]['id'];
}
// Generate a fake qfKey in case payment processor redirects to contribution thank-you page
$params['qfKey'] = $this->getQfKey();
$params['contactID'] = $params['contact_id'];
$params['currency'] = $params['currencyID'] = wf_crm_aval($this->data, "contribution:1:currency");
$params['total_amount'] = round($this->totalContribution, 2);
// Some processors want this one way, some want it the other
$params['amount'] = $params['total_amount'];
$numInstallments = wf_crm_aval($params, 'installments', NULL, TRUE);
$frequencyInterval = wf_crm_aval($params, 'frequency_unit');
if ($numInstallments == 1 || empty($frequencyInterval)) {
unset($params['frequency_unit'], $params['frequency_interval'], $params['is_recur']);
}
$params['financial_type_id'] = wf_crm_aval($this->data, 'contribution:1:contribution:1:financial_type_id');
$params['source'] = $this->settings['new_contact_source'];
$params['item_name'] = $params['description'] = t('Webform Payment: @title', ['@title' => $this->node->label()]);
if (method_exists($paymentProcessor, 'setSuccessUrl')) {
$params['successURL'] = $this->getIpnRedirectUrl('success');
$params['cancelURL'] = $this->getIpnRedirectUrl('cancel');
}
$this->form_state->set(['civicrm', 'doPayment'], $params);
}
/**
* @param $type
* @return string
*/
public function getIpnRedirectUrl($type) {
$confirmation_type = $this->node->getSetting('confirmation_type');
$url = trim($this->node->getSetting('confirmation_url'));
if ($type === 'cancel') {
$url = $this->node->toUrl('canonical', ['absolute' => TRUE])->toString();
}
elseif ($confirmation_type === WebformInterface::CONFIRMATION_PAGE) {
$request_handler = \Drupal::getContainer()->get('webform.request');
$query = ['sid' => $this->submission->id()];
// Add token if user is not authenticated, inline with 'webform_client_form_submit()'
if (\Drupal::currentUser()->isAnonymous()) {
// @todo look up how this works in D8.
// $query['token'] = webform_get_submission_access_token($this->submission);
}
$url = $request_handler->getUrl($this->node, $this->node, 'webform.confirmation', ['query' => $query, 'absolute' => TRUE])->toString();
}
else {
// $parsed = webform_replace_url_tokens($url, $this->node, $this->submission);
// $parsed[1]['absolute'] = TRUE;
// $url = url($parsed[0], $parsed[1]);
}
return $url;
}
/**
* Format contribution params for transact/pay-later
* @return array
*/
private function contributionParams() {
$params = $this->billing_params + $this->data['contribution'][1]['contribution'][1];
$params['financial_type_id'] = wf_crm_aval($this->data, 'contribution:1:contribution:1:financial_type_id');
$params['currency'] = $params['currencyID'] = wf_crm_aval($this->data, "contribution:1:currency");
$params['skipRecentView'] = $params['skipLineItem'] = 1;
$i = $this->getContributionContactIndex();
$params['contact_id'] = $this->ent['contact'][$i]['id'];
$params['total_amount'] = round($this->totalContribution, 2);
// Most payment processors expect this (normally be set by contribution page processConfirm)
$params['contactID'] = $params['contact_id'];
// Some processors use this for matching and updating the contribution status
if (!$this->contributionIsPayLater) {
$params['invoice_id'] = $this->data['contribution'][1]['contribution'][1]['invoiceID'] = md5(uniqid(mt_rand(), TRUE));
}
// Sales tax integration
$params['tax_amount'] = 0;
if (isset($this->form_state->getStorage()['civicrm']['line_items'])) {
foreach ($this->form_state->getStorage()['civicrm']['line_items'] as $lineItem) {
if (!empty($lineItem['tax_amount'])) {
$params['tax_amount'] += $lineItem['tax_amount'];
}
}
$params['tax_amount'] = round($params['tax_amount'], 2);
}
$params['description'] = t('Webform Payment: @title', ['@title' => $this->node->label()]);
if (!isset($params['source'])) {
$params['source'] = $this->settings['new_contact_source'];
}
$financialTypeId = $params['financial_type_id'];
$financialTypeDetails = $this->utils->wf_civicrm_api('FinancialType', 'get', ['return' => "is_deductible,name",'id' => $financialTypeId]);
// Some payment processors expect this to be set (eg. smartdebit)
$params['financialType_name'] = $financialTypeDetails['values'][$financialTypeId]['name'];
$params['financialTypeID'] = $financialTypeId;
// Calculate non-deductible amount for income tax purposes
if ($financialTypeDetails['values'][$financialTypeId]['is_deductible'] == 1) {
// deductible
$params['non_deductible_amount'] = 0;
} else {
$params['non_deductible_amount'] = $params['total_amount'];
}
// Pass all submitted values to payment processor.
$request = \Drupal::request();
foreach ($request->request->all() as $key => $value) {
if (empty($params[$key])) {
$params[$key] = $value;
}
}
if (empty($params['payment_instrument_id']) && !empty($params['payment_processor_id'])) {
$params['payment_instrument_id'] = $this->getPaymentInstrument($params['payment_processor_id']);
}
// doPayment requries payment_processor and payment_processor_mode fields.
$params['payment_processor'] = wf_crm_aval($params, 'payment_processor_id');
if (!empty($params['credit_card_number'])) {
if (!empty($params['credit_card_type'])) {
$result = $this->utils->wf_civicrm_api('OptionValue', 'get', [
'return' => ['value'],
'option_group_id.name' => 'accept_creditcard',
'name' => ucfirst($params['credit_card_type']),
]);
if (!empty($result['id'])) {
$params['card_type_id'] = $result['values'][$result['id']]['value'];
}
}
$params['pan_truncation'] = substr($params['credit_card_number'], -4);
}
// Save this stuff for later
unset($params['soft'], $params['soft_credit_type_id']);
return $params;
}
/**
* Post-processing of contribution
* This happens during form post-processing
*/
private function processContribution() {
$contribution = $this->data['contribution'][1]['contribution'][1];
$id = $this->ent['contribution'][1]['id'];
// Save soft credits
if (!empty($contribution['soft'])) {
// Get total amount from total amount after line item calculation
$amount = $this->totalContribution;
// Get Default softcredit type
$default_soft_credit_type = $this->utils->wf_civicrm_api('OptionValue', 'getsingle', [
'return' => "value",
'option_group_id' => "soft_credit_type",
'is_default' => 1,
]);
foreach (array_filter($contribution['soft']) as $cid) {
$this->utils->wf_civicrm_api('contribution_soft', 'create', [
'contact_id' => $cid,
'contribution_id' => $id,
'amount' => $amount,
'currency' => wf_crm_aval($this->data, "contribution:1:currency"),
'soft_credit_type_id' => $contribution['soft_credit_type_id'] ?? $default_soft_credit_type['value'],
]);
}
}
$contributionResult = \CRM_Contribute_BAO_Contribution::getValues(['id' => $id], \CRM_Core_DAO::$_nullArray, \CRM_Core_DAO::$_nullArray);
// Save line-items
foreach ($this->line_items as &$item) {
if (empty($item['line_total']) && $item['entity_table'] != 'civicrm_membership') {
continue;
}
if (empty($item['entity_id'])) {
$item['entity_id'] = $id;
}
// tax integration
if (empty($item['contribution_id'])) {
$item['contribution_id'] = $id;
}
$priceSetId = 'default_contribution_amount';
// Membership
if (!empty($item['membership_id'])) {
$priceSetId = 'default_membership_type_amount';
$item['entity_id'] = $item['membership_id'];
$lineItemArray = $this->utils->wf_civicrm_api('LineItem', 'get', [
'entity_table' => "civicrm_membership",
'entity_id' => $item['entity_id'],
]);
if ($lineItemArray['count'] != 0) {
// We only require first membership (signup) entry to make this work.
$firstLineItem = array_shift($lineItemArray['values']);
// Membership signup line item entry.
// Line Item record is already present for membership by this stage.
// Just need to upgrade contribution_id column in the record.
if (!isset($firstLineItem['contribution_id'])) {
$item['id'] = $firstLineItem['id'];
}
}
}
$item['price_field_id'] = $this->utils->wf_civicrm_api('PriceField', 'get', [
'sequential' => 1,
'price_set_id' => $priceSetId,
'options' => ['limit' => 1],
])['id'] ?? NULL;
// Save the line_item
$line_result = $this->utils->wf_civicrm_api('line_item', 'create', $item);
$item['id'] = $line_result['id'];
$lineItemRecord = json_decode(json_encode($item), FALSE);
// Add financial_item and entity_financial_trxn
// hence that we call \CRM_Financial_BAO_FinancialItem::add() twice,
// once to create the line item 'total amount' financial_item record and the 2nd
// one to create the line item 'tax amount' financial_item record.
\CRM_Financial_BAO_FinancialItem::add($lineItemRecord, $contributionResult);
if (!empty($lineItemRecord->tax_amount)) {
\CRM_Financial_BAO_FinancialItem::add($lineItemRecord, $contributionResult, TRUE);
}
// Create participant/membership payment records
if (isset($item['membership_id']) || isset($item['participant_id'])) {
$type = isset($item['participant_id']) ? 'participant' : 'membership';
$this->utils->wf_civicrm_api("{$type}_payment", 'create', [
"{$type}_id" => $item["{$type}_id"],
'contribution_id' => $id,
]);
}
}
}
/**
* @param string $ent - entity type
* @param int $n - entity number
* @param int $id - entity id
* @param bool $new - is this a new object? (should we bother checking for existing data)
*/
private function processAttachments($ent, $n, $id, $new = FALSE) {
$attachments = $new ? [] : $this->getAttachments($ent, $id);
foreach ((array) wf_crm_aval($this->data[$ent], "$n:{$ent}upload:1") as $num => $file_id) {
if ($file_id) {
[, $i] = explode('_', $num);
$dao = new \CRM_Core_DAO_EntityFile();
if (!empty($attachments[$i])) {
$dao->id = $attachments[$i]['id'];
}
$dao->file_id = $file_id;
$dao->entity_id = $id;
$dao->entity_table = "civicrm_$ent";
$dao->save();
}
}
}
/**
* Recursive function to fill ContactRef fields with contact IDs
*
* @param $contactPrefillMode
* TRUE if only standard custom fields needs to be filled.
* This is a pre-fill mode which sets a placeholder value if the referenced contact hasn't been saved yet.
* @internal param $values null|array
* Leave blank - used internally to recurse through data
* @internal param $depth int
* Leave blank - used internally to track recursion level
*/
private function fillContactRefs($contactPrefillMode = FALSE, $values = NULL, $depth = 0) {
$order = ['ent', 'c', 'table', 'n', 'name'];
static $ent = '';
static $c = '';
static $table = '';
static $n = '';
if ($values === NULL) {
$values = $this->data;
}
foreach ($values as $key => $val) {
${$order[$depth]} = $key;
if ($depth < 4 && is_array($val)) {
$this->fillContactRefs($contactPrefillMode, $val, $depth + 1);
}
elseif ($depth == 4 && $val && wf_crm_aval($this->all_fields, "{$table}_{$name}:data_type") == 'ContactReference') {
// Standard custom fields are processed with their entity; fields from multi-record sets are processed separately
$isStandardCustom = substr($name, 0, 6) === 'custom' && !$this->isMultiRecordCustomSet($table);
if ($contactPrefillMode && (!$isStandardCustom || $ent !== 'contact')) {
return;
}
if (is_array($val)) {
$this->data[$ent][$c][$table][$n][$name] = [];
foreach ($val as $v) {
if (is_numeric($v) && !empty($this->ent['contact'][$v]['id'])) {
$tableName = $isStandardCustom ? $ent : $table;
$this->data[$ent][$c][$tableName][$n][$name][] = $this->ent['contact'][$v]['id'];
}
}
}
else {
// Unset val so it is not mistaken for a contact id
$this->data[$ent][$c][$table][$n][$name] = NULL;
if (!empty($this->ent['contact'][$val]['id'])) {
$tableName = $isStandardCustom ? $ent : $table;
$this->data[$ent][$c][$tableName][$n][$name] = $this->ent['contact'][$val]['id'];
}
elseif ($contactPrefillMode) {
$this->data[$ent][$c]['update_contact_ref'][$n][$name] = $val;
}
}
}
}
}
/**
* Fill data array with submitted form values
*/
private function fillDataFromSubmission() {
foreach ($this->enabled as $field_key => $fid) {
$val = (array) $this->submissionValue($fid);
$customValue = NULL;
// If value is null then it was hidden by a webform conditional rule - skip it
if ($val !== NULL && $val !== [NULL]) {
[ , $c, $ent, $n, $table, $name] = explode('_', $field_key, 6);
// The following are not strictly CiviCRM fields, so ignore
if (in_array($name, ['existing', 'fieldset', 'createmode'])) {
continue;
}
// Check to see if configured to ignore hidden field value(s)
if ($this->isFieldHiddenByExistingContactSettings($ent, $c, $table, $n, $name, TRUE)) {
continue;
}
$field = $this->all_fields[$table . '_' . $name];
$component = $this->node->getElementDecoded($field_key);
// Ignore values from hidden fields
if ($field['type'] === 'hidden') {
continue;
}
// Translate privacy options into separate values
if ($name === 'privacy') {
foreach (array_keys($this->getExposedOptions($field_key)) as $key) {
$this->data[$ent][$c][$table][$n][$key] = in_array($key, $val);
}
continue;
}
$dataType = wf_crm_aval($field, 'data_type');
if (!empty($field['extra']['multiple'])) {
if ($val === ['']) {
$val = [];
}
// Merge with existing data
if (!empty($this->data[$ent][$c][$table][$n][$name]) && is_array($this->data[$ent][$c][$table][$n][$name])) {
$val = array_unique(array_merge($val, $this->data[$ent][$c][$table][$n][$name]));
}
if (substr($name, 0, 6) === 'custom' || ($table == 'other' && in_array($name, ['group', 'tag']))) {
$val = array_filter($val);
if ($name === 'group') {
unset($val['public_groups']);
}
}
// We need to handle items being de-selected too and provide an array to pass to Entity.create API
// Extract a list of available items
$exposedOptions = $this->getExposedOptions($field_key);
$customValue = [];
foreach ($exposedOptions as $itemName => $itemLabel) {
if (in_array($itemName, $val)) {
$customValue[] = $itemName;
}
}
// Implode data that will be stored as a string
if ($table !== 'other' && $name !== 'event_id' && $name !== 'relationship_type_id' && $table !== 'contact' && $dataType != 'ContactReference' && strpos($name, 'tag') !== 0) {
$val = \CRM_Utils_Array::implodePadded($val);
}
}
// If this is a single-static radio button, set it to a non array value
// since civicrm api doesn't expect array for a radio field.
elseif (empty($component['#extra']['multiple']) && $this->utils->hasMultipleValues($component)) {
$val = $val[0] ?? NULL;
}
elseif ($name === 'image_url') {
if (empty($val[0]) || !($val = $this->getDrupalFileUrl($val[0]))) {
// This field can't be emptied due to the nature of file uploads
continue;
}
}
elseif ($dataType == 'File') {
// Replace filename (with tokens) if set.
if (isset($component['#file_name']) && $component['#file_name']) {
$newFilename = $this->handler->tokenManager->replace($component['#file_name'], $this->submission);
}
if (empty($val[0]) || !($val = $this->saveDrupalFileToCivi($val[0], $newFilename))) {
// This field can't be emptied due to the nature of file uploads
continue;
}
}
elseif ($field['type'] === 'date') {
$val = empty($val[0]) ? '' : str_replace('-', '', $val[0]);
// Add time field value
$time = wf_crm_aval($this->data, "$ent:$c:$table:$n:$name", '');
// Remove default date if it has been added
if (strlen($time) == 14) {
$time = substr($time, -6);
}
$val .= $time;
}
// The admin can change a number field to use checkbox/radio/select/grid widget and we'll sum the result
elseif ($field['type'] === 'number' || $field['type'] === 'civicrm_number') {
$sum = 0;
foreach ((array) $val as $k => $v) {
// Perform multiplication across grid elements
if (is_numeric($k) && $component['#type'] === 'grid') {
$v *= $k;
}
if (is_numeric($v)) {
$sum += $v;
}
}
// Do not constrain to only allow positive amounts [note fields like contribution_amount come with a minimum 0 value - so admin must be specific/ok this]
$val = $sum;
}
else {
$val = is_array($val) ? reset($val) : $val;
// Trim whitespace from user input
if (is_string($val)) {
$val = trim($val);
}
}
if (is_string($val) && '' !== $val && $field['type'] === 'autocomplete') {
$options = $this->utils->wf_crm_field_options($component, '', $this->data);
$val = array_search($val, $options);
}
// Only known contacts are allowed to empty a field
if (($val !== '' && $val !== NULL && $val !== []) || !empty($this->existing_contacts[$c])) {
$this->data[$ent][$c][$table][$n][$name] = $val;
if (substr($name, 0, 6) === 'custom' && !$this->isMultiRecordCustomSet($table)) {
$this->data[$ent][$c][$ent][$n][$name] = $customValue ?? $val;
}
}
}
}
}
/**
* Test whether a field has been hidden due to existing contact settings
* @param $ent
* @param $c
* @param $table
* @param $n
* @param $name
* @param $checkSubmitDisabledSetting
* Checks if 'Submit Disabled' setting should be considered
*
* @return bool
*/
private function isFieldHiddenByExistingContactSettings($ent, $c, $table, $n, $name, $checkSubmitDisabledSetting = FALSE) {
if ($ent === 'contact' && isset($this->enabled["civicrm_{$c}_contact_1_contact_existing"])) {
$component = $this->node->getElement("civicrm_{$c}_contact_1_contact_existing");
$existing_contact_val = $this->submissionValue($component['#form_key']);
// Fields are hidden if value is empty (no selection) or a numeric contact id
if (!$existing_contact_val[0] || is_numeric($existing_contact_val[0])) {
$type = ($table == 'contact' && strpos($name, 'name')) ? 'name' : $table;
$component += ['#hide_fields' => []];
if (in_array($type, $component['#hide_fields'])) {
$value = wf_crm_aval($this->loadContact($c), "$table:$n:$name");
// Check to see if configured to Submit disabled field value(s)
if ($checkSubmitDisabledSetting && !empty($component['#submit_disabled'])) {
// If field is disabled on the webform, do not overwrite existing values on the contact.
if (!empty($value) && !empty($this->submission)) {
$fieldKey = implode('_', ['civicrm', $c, $ent, $n, $table, $name]);
$data = $this->submission->getData();
if (isset($data[$fieldKey])) {
$data[$fieldKey] = $value;
$this->submission->setData($data);
}
}
else {
return FALSE;
}
}
// With the no_hide_blank setting we must load the contact to determine if the field was hidden
if (wf_crm_aval($component, '#no_hide_blank')) {
// @todo this method doesn't exist?
return !(!$value && !is_numeric($value));
}
return TRUE;
}
}
}
return FALSE;
}
/**
* Test if any relevant data has been entered for a location
* @param string $location
* @param array $params
* @return bool
*/
private function locationIsEmpty($location, $params) {
switch ($location) {
case 'address':
return empty($params['street_address'])
&& empty($params['city'])
&& empty($params['state_province_id'])
&& empty($params['country_id'])
&& empty($params['postal_code'])
&& (empty($params['master_id']) || $params['master_id'] == 'null');
case 'website':
return empty($params['url']);
case 'im':
return empty($params['name']);
default:
return empty($params[$location]);
}
}
/**
* Clears an error against a form element.
* Used to disable validation when this module hides a field
* @see https://api.drupal.org/comment/49163#comment-49163
*
* @param $name string
*/
private function unsetError($name) {
$errors = &drupal_static('form_set_error', []);
$removed_messages = [];
if (isset($errors[$name])) {
$removed_messages[] = $errors[$name];
unset($errors[$name]);
}
$_SESSION['messages']['error'] = array_diff($_SESSION['messages']['error'], $removed_messages);
if (empty($_SESSION['messages']['error'])) {
unset($_SESSION['messages']['error']);
}
}
/**
* Get or set a value from a webform submission
* Always use \Drupal\webform\WebformSubmissionInterface $webform_submission [more reliable according to JR]
*
* @param $fid
* Numeric webform component id
* @param $value
* Value to set - leave empty to get a value rather than setting it
*
* @return array|null field value if found
*/
protected function submissionValue($fid, $value = NULL) {
if (!empty($this->form_state)) {
$form_object = $this->form_state->getFormObject();
/** @var \Drupal\webform\WebformSubmissionInterface $webform_submission */
$webform_submission = $form_object->getEntity();
$data = $webform_submission->getData();
}
else {
$webform_submission = $this->submission;
$data = $this->submission->getData();
$webform_submission = $this->submission;
}
if (!isset($data[$fid])) {
return [NULL];
}
// Expects an array.
$field = (array) $data[$fid];
// During submission preprocessing this is used to alter the submission
if ($value !== NULL) {
$data[$fid] = $value;
$webform_submission->setData($data);
return $value;
}
return $field;
}
/**
* Identifies contact 1 as acting user for CiviCRM's advanced logging
*/
public function setLoggingContact() {
if (!empty($this->ent['contact'][1]['id']) && \Drupal::currentUser()->isAnonymous()) {
\CRM_Core_DAO::executeQuery('SET @civicrm_user_id = %1', [1 => [$this->ent['contact'][1]['id'], 'Integer']]);
}
}
/**
* Reorder submitted location values according to existing location values
*
* @param array $submittedLocationValues
* @param array $existingLocationValues
* @param string $entity
* @return array
*/
protected function reorderLocationValues($submittedLocationValues, $existingLocationValues, $entity) {
$reorderedArray = [];
$index = 1;
$entityTypeIdIndex = $entity.'_type_id';
$entity = $entity == 'website' ? 'url' : $entity; // for website only
foreach ($existingLocationValues as $eValue) {
$existingLocationTypeId = $entity != 'url' ? $eValue['location_type_id'] : NULL;
$existingEntityTypeId = isset($eValue[$entityTypeIdIndex]) ? $eValue[$entityTypeIdIndex] : NULL;
if (!empty($existingEntityTypeId)) {
$reorderedArray[$index][$entityTypeIdIndex] = $existingEntityTypeId;
}
elseif (!empty($existingLocationTypeId)) {
$reorderedArray[$index]['location_type_id'] = $existingLocationTypeId;
}
if (!empty($eValue[$entity])) {
$reorderedArray[$index][$entity] = $eValue[$entity];
}
// address field contain many sub fields and should be handled differently
if ($entity != 'address') {
$submittedLocationValues = self::unsetEmptyValueIndexes($submittedLocationValues, $entity);
$reorderedArray[$index][$entity] = wf_crm_aval($eValue, $entity);
}
else {
foreach ($this->utils->wf_crm_address_fields() as $field) {
if ($field != 'location_type_id') {
$reorderedArray[$index][$field] = isset($eValue[$field]) ? $eValue[$field] : '';
}
}
}
foreach ($submittedLocationValues as $key => $sValue) {
$sLocationTypeId = isset($sValue['location_type_id']) ? $sValue['location_type_id'] : NULL;
$sEntityTypeId = isset($sValue[$entityTypeIdIndex]) ? $sValue[$entityTypeIdIndex] : NULL;
if (($existingLocationTypeId == $sLocationTypeId && empty($sEntityTypeId))
||
($existingEntityTypeId == $sEntityTypeId && empty($sLocationTypeId))
||
($existingLocationTypeId == $sLocationTypeId && $existingEntityTypeId == $sEntityTypeId)) {
// address field contain many sub fields and should be handled differently
if ($entity != 'address') {
$reorderedArray[$index] = $sValue;
unset($submittedLocationValues[$key]);
break;
}
else {
foreach ($this->utils->wf_crm_address_fields() as $field) {
if (isset($sValue[$field]) && $field != 'location_type_id') {
$reorderedArray[$index][$field] = $sValue[$field];
}
}
}
unset($submittedLocationValues[$key]);
}
}
$index++;
}
// handle remaining values
if (!empty($submittedLocationValues)) {
// cannot use array_push, array_merge because preserving array keys is important
foreach ($submittedLocationValues as $sValue) {
$reorderedArray[] = $sValue;
}
}
return $reorderedArray;
}
/**
* @param array $contact
* @param int $index
* @param int $cid
*/
private function saveMultiRecordCustomData($contact, $index, $cid) {
$existingCustomData = NULL;
$inserts = [];
$saveParams = ['id' => $cid];
foreach ($contact as $table => $items) {
if (is_array($items) && $items && $this->isMultiRecordCustomSet($table)) {
$existingCustomData = $existingCustomData ?? $this->getCustomData($cid, 'contact', FALSE, $index);
$existing = $existingCustomData[$table] ?? [];
// Index existing ids beginning with 1
$existingIds = array_slice(array_merge([NULL], array_keys($existing)), 1, NULL, TRUE);
$newIndex = 0;
foreach ($items as $i => $item) {
$id = $existingIds[$i] ?? --$newIndex;
if ($id > 0) {
$this->ent['contact'][$index][$table][$i] = $existingIds[$i];
}
else {
$inserts[$table][] = $i;
}
foreach ($item as $field => $val) {
$saveParams[$field . '_' . $id] = $val;
}
}
}
}
if (count($saveParams) > 1) {
$utils = \Drupal::service('webform_civicrm.utils');
$utils->wf_civicrm_api('Contact', 'create', $saveParams);
// FIXME: This sucks, but APIv3 does not return the id of newly inserted custom data
foreach ($inserts as $table => $newRecords) {
$dbTable = \CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomGroup', str_replace('cg', '', $table), 'table_name');
$maxId = \CRM_Core_DAO::singleValueQuery("SELECT MAX(id) FROM $dbTable");
foreach ($newRecords as $count => $i) {
$this->ent['contact'][$index][$table][$i] = $maxId - count($newRecords) + $count + 1;
}
}
}
}
/**
* Save custom contact reference created
* during the current submission of the webform.
*
* @param array $params
* @param int $cid
*/
private function saveContactRefs($params, $cid): void {
$updateParams = [
'id' => $cid,
];
$skipKeys = [];
foreach ($params['update_contact_ref'] as $n => $refKeys) {
foreach ($refKeys as $refKey => $val) {
// Skip contact ref that doesn't have a valid contact ids.
if (empty($this->ent['contact'][$val]['id'])) {
continue;
}
foreach ($params['contact'] as $contactParams) {
foreach ($contactParams as $key => $value) {
if (strpos($key, "{$refKey}_") === 0 && !isset($updateParams[$key]) && !in_array($key, $skipKeys)) {
$updateParams[$key] = $value;
}
}
}
}
}
if (count($updateParams) > 1) {
$this->utils->wf_civicrm_api('contact', 'create', $updateParams);
}
}
private function unsetEmptyValueIndexes($values, $entity) {
foreach ($values as $k => $v) {
if (!isset($v[$entity])) {
unset($values[$k]);
}
}
return $values;
}
/**
* Retrieve payment instrument.
*
* @param int $paymentProcessorId
*/
private function getPaymentInstrument($paymentProcessorId) {
$processor = \Civi\Payment\System::singleton()->getById($paymentProcessorId);
return $processor->getPaymentInstrumentID();
}
/**
* Check if a set of data is for a multi-record custom field
* @param $key e.g. "cg5"
* @return bool
*/
private function isMultiRecordCustomSet($key) {
return strpos($key, 'cg') === 0 && ($this->all_sets[$key]['max_instances'] ?? 1) > 1;
}
}
