You are here

class WebformCivicrmPostProcess in Webform CiviCRM Integration 8.5

Hierarchy

Expanded class hierarchy of WebformCivicrmPostProcess

1 string reference to 'WebformCivicrmPostProcess'
webform_civicrm.services.yml in ./webform_civicrm.services.yml
webform_civicrm.services.yml
1 service uses WebformCivicrmPostProcess
webform_civicrm.postprocess in ./webform_civicrm.services.yml
Drupal\webform_civicrm\WebformCivicrmPostProcess

File

src/WebformCivicrmPostProcess.php, line 19
Front-end form validation and post-processing.

Namespace

Drupal\webform_civicrm
View source
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\WebformSubmissionInterface
   */
  private $submission;
  private $update = [];
  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;
  function initialize(WebformInterface $webform) {
    if ($this->initialized) {
      return $this;
    }
    $this->node = $webform;
    $handler_collection = $webform
      ->getHandlers('webform_civicrm');
    $instance_ids = $handler_collection
      ->getInstanceIds();
    $handler = $handler_collection
      ->get(reset($instance_ids));
    $utils = \Drupal::service('webform_civicrm.utils');
    $this->database = \Drupal::database();
    $this->settings = $handler
      ->getConfiguration()['settings'];
    $this->data = $this->settings['data'];
    $this->enabled = $utils
      ->wf_crm_enabled_fields($webform);
    $this->all_fields = $utils
      ->wf_crm_get_fields();
    $this->all_sets = $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 array $form_state (reference)
   */
  public function validate($form, FormStateInterface $form_state, WebformSubmissionInterface $webform_submission) {
    $this->form = $form;
    $this->form_state = $form_state;
    $this->rawValues = $form_state
      ->getValues();
    $utils = \Drupal::service('webform_civicrm.utils');
    $this->crmValues = $utils
      ->wf_crm_enabled_fields($webform_submission
      ->getWebform(), $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',
    ]) ?: [];
    $errors = $this->form_state
      ->getErrors();
    foreach ($errors as $key => $error) {
      $pieces = $utils
        ->wf_crm_explode_key(substr($key, strrpos($key, '][') + 2));
      if ($pieces) {
        list(, $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);
    $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['#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);

    // Check for existing submission
    // $this->setUpdateParam();
    // 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']);

      // 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();

    // 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)) {
      $template = \CRM_Core_Smarty::singleton();
      $template
        ->assign('dataArray', [
        "{$this->tax_rate}" => $this->tax_rate / 100,
      ]);
    }
    if ($this->contributionIsIncomplete) {
      $template = \CRM_Core_Smarty::singleton();
      $template
        ->assign('is_pay_later', 1);
    }
    \Drupal::service('webform_civicrm.utils')
      ->wf_civicrm_api('contribution', 'sendconfirmation', $this
      ->getReceiptParams());
  }

  /**
   * Build params for contribution receipt.
   *
   * @return array
   */
  private function getReceiptParams() {
    $contributionData = wf_crm_aval($this->data, 'contribution:1:contribution:1');
    $params = [
      'id' => $this->ent['contribution'][1]['id'],
    ];
    $params['payment_processor_id'] = $contributionData['payment_processor_id'];
    unset($params['payment_processor']);
    $params['financial_type_id'] = $contributionData['financial_type_id'];
    $params['currency'] = wf_crm_aval($this->data, "contribution:1:currency");

    //Assign receipt values set on the webform config page.
    $receipt = wf_crm_aval($this->data, "receipt", []);
    $receiptValues = [
      'cc_receipt',
      'bcc_receipt',
      'receipt_text',
      'pay_later_receipt',
      'receipt_from_name',
      'receipt_from_email',
    ];
    foreach ($receiptValues as $val) {
      $params[$val] = $receipt["number_number_of_receipt_{$val}"] ?? '';
    }
    return $params;
  }

  /**
   * Formats submission data as expected by the schema
   */
  private function formatSubmission() {
    $data = $this->ent;
    unset($data['contact']);
    $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.
    $utils = \Drupal::service('webform_civicrm.utils');
    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) $utils
              ->wf_crm_get_civi_setting('defaultContactCountry', 1228);
          }
          $isBilling = strpos($element['#name'], 'billing_address_') !== false ?? FALSE;
          $states = $utils
            ->wf_crm_get_states($country_id, $isBilling);
          if ($states && !array_key_exists(strtoupper($element['#value']), $states)) {
            $countries = $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'],
            ]));
          }
        }
        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;
    $utils = \Drupal::service('webform_civicrm.utils');

    // 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 = $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) {
          list($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 = $utils
        ->wf_crm_apivalues('participant_status_type', 'get', [
        'is_counted' => 1,
        'return' => 'id',
      ]);
      $existing = $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) {
        if (isset($this->events[$participant['event_id']])) {
          if (!--$this->events[$participant['event_id']]['count']) {
            unset($this->events[$participant['event_id']]);
          }
        }
        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]);
            }
          }
        }
      }
    }
    $this
      ->loadEvents();

    // Add event info to line items
    $format = wf_crm_aval($this->data['reg_options'], 'title_display', 'title');
    foreach ($this->line_items as &$item) {
      $label = empty($item['contact_label']) ? '' : "{$item['contact_label']} - ";
      $item['label'] = $label . $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', 'Event Fee');
    }

    // 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 {
            $this->form_state
              ->setErrorByName($eid, format_plural($event['available_places'], 'Sorry, you tried to register !count people for %event but there is only 1 space remaining.', 'Sorry, you tried to register !count people for %event but there are only @count spaces remaining.', [
              '%event' => $event['title'],
              '@count' => $event['count'],
            ]));
          }
        }
      }
    }
  }

  /**
   * 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];
        }
        elseif (\Drupal::service('webform_civicrm.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();
    }
  }

  /**
   * If this is an update op, set param for drupal_write_record()
   */
  private function setUpdateParam() {
    if (!empty($this->submission->sid)) {
      $submitted = [
        $this->submission->sid => new stdClass(),
      ];
      webform_civicrm_webform_submission_load($submitted);
      if (isset($submitted[$this->submission->sid]->civicrm)) {
        $this->update = 'sid';
      }
    }
  }

  /**
   * Fetch contact ids from "existing contact" fields
   */
  private function getExistingContactIds() {
    foreach ($this->enabled as $field_key => $fid) {
      if (substr($field_key, -8) === 'existing') {
        list(, $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) {
    $utils = \Drupal::service('webform_civicrm.utils');
    if ($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'];
    $utils = \Drupal::service('webform_civicrm.utils');
    foreach ($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);
    $utils = \Drupal::service('webform_civicrm.utils');
    if ($rule) {
      $contact['contact'][1]['contact_type'] = ucfirst($contact['contact'][1]['contact_type']);
      $params = [
        'check_permission' => 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];
        }
        elseif (!empty($this->existing_contacts[$m])) {
          $masters = $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);
          }
        }
      }

      // Translate state abbr to id (skip if using $masters address which would have returned id not abbr from the api)
      if (empty($masters) && !empty($contact['address'][1]['state_province_id'])) {
        $contact['address'][1]['state_province_id'] = $utils
          ->wf_crm_state_abbr($contact['address'][1]['state_province_id'], 'id', $contact['address'][1]['country_id'] ?? NULL);
      }
      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 = $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];
    $utils = \Drupal::service('webform_civicrm.utils');

    // 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 = $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];
    $utils = \Drupal::service('webform_civicrm.utils');
    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') {
        list(, $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 = $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'];
    $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'])) {
      \Drupal::service('webform_civicrm.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) {
    $utils = \Drupal::service('webform_civicrm.utils');
    $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 = $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 = $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) {
    $utils = \Drupal::service('webform_civicrm.utils');

    // 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 ($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',
          ];
        }
        $result = $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) {

          // Translate state/prov abbr to id
          if (!empty($params['state_province_id'])) {
            $default_country = $utils
              ->wf_crm_get_civi_setting('defaultContactCountry', 1228);
            if (!($params['state_province_id'] = $utils
              ->wf_crm_state_abbr($params['state_province_id'], 'id', wf_crm_aval($params, 'country_id', $default_country)))) {
              $params['state_province_id'] = '';
            }
          }

          // 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'])) {
              $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;
            }
          }
          $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) {
      list($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 = []) {
    $utils = \Drupal::service('webform_civicrm.utils');
    $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 = $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 = $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 = $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 = $utils
            ->wf_civicrm_api('mailing_event_subscribe', 'create', $mailer_params);
          if (empty($result['is_error'])) {
            $confirmations_sent[] = Html::escape($mailing_lists[$a]);
          }
          else {
            $utils
              ->wf_civicrm_api($api, 'create', $params);
          }
        }
        else {
          $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;
      $utils
        ->wf_civicrm_api($api, 'delete', $params);
    }
    if (!empty($remove) && $data_type == 'group') {
      $display_name = $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) {
    $utils = \Drupal::service('webform_civicrm.utils');
    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,
      ];
      $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) {
    $utils = \Drupal::service('webform_civicrm.utils');
    if (!empty($params['relationship_type_id']) && $cid2 && $cid1 != $cid2) {
      list($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) {
        list($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']);
      $utils
        ->wf_civicrm_api('relationship', 'create', $params);
    }
  }

  /**
   * Process event participation for a contact
   * @param int $c
   * @param int $cid
   */
  private function processParticipants($c, $cid) {
    $utils = \Drupal::service('webform_civicrm.utils');
    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 = $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) {
              list($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)) {
              list($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']);
              }
              $result = $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) {
                $registered_by_id[$e][$i] = $result['id'];
              }
            }
          }
        }
        foreach ($remove as $eid => $title) {
          $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;
    $utils = \Drupal::service('webform_civicrm.utils');
    if (!isset($types)) {
      $types = $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;
          }
        }
      }
      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 = $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 = $utils
            ->wf_civicrm_api('MembershipType', 'getsingle', [
            'id' => $membership['membership_type_id'],
          ]);
          $memStatus = $utils
            ->wf_civicrm_api('MembershipStatus', 'getsingle', [
            'id' => $membership['status_id'],
          ]);
          $actParams['subject'] = ts("%1 - Status: %2", [
            1 => $memType['name'],
            2 => $memStatus['label'],
          ]);
          $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() {
    $utils = \Drupal::service('webform_civicrm.utils');
    foreach ($this->shared_address as $cid => $shared) {
      foreach ($shared as $i => $addr) {
        if (!empty($this->ent['contact'][$addr['mc']]['id'])) {
          $masters = $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'];
            $utils
              ->wf_civicrm_api('address', 'create', $params);
          }
        }
      }
    }
  }

  /**
   * Save case data
   */
  private function processCases() {
    $utils = \Drupal::service('webform_civicrm.utils');
    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 = $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'] = $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;
            }
          }
        }
        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 = $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 = $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 = $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;
                  }
                  $utils
                    ->wf_civicrm_api('relationship', 'create', $relationshipParams);
                }
              }
            }
          }
        }
      }
    }
  }

  /**
   * Save activity data
   */
  private function processActivities() {
    $utils = \Drupal::service('webform_civicrm.utils');
    $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'] = $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);
        }
        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 = $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'];
            }
          }
          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']) || !$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 = $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 ($utils
              ->wf_crm_get_civi_setting('activity_assignee_notification')) {

              // Send email to assignees. TODO: Move to CiviCRM API?
              $assignees = $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) {
      list($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>';
    }
    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() {
    $utils = \Drupal::service('webform_civicrm.utils');
    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 = $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';
          }
        }
        else {
          $params['id'] = $this->ent['grant'][$n]['id'];
        }

        // Allow "automatic" status to pass-thru
        if (empty($params['status_id'])) {
          unset($params['status_id']);
        }
        $result = $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() {

    // Contribution
    $utils = \Drupal::service('webform_civicrm.utils');
    $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 ($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) {
              $this->line_items[] = [
                'qty' => $membership_item['num_terms'],
                'unit_price' => $price,
                'financial_type_id' => $membership_financialtype,
                'label' => $this
                  ->getMembershipTypeField($type, 'name') . ": " . $utils
                  ->wf_crm_display_name($this->existing_contacts[$c]),
                '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['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
    for ($i = 1; $i <= $this->data['contact'][1]['number_of_email']; ++$i) {
      if (!empty($this->crmValues["civicrm_1_contact_{$i}_email_email"])) {
        $params['email'] = $this->crmValues["civicrm_1_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;
  }

  /**
   * Create contact 1 if not already existing (required by contribution.transact)
   * @return int
   */
  private function createBillingContact() {
    $cid = wf_crm_aval($this->existing_contacts, 1);
    $utils = \Drupal::service('webform_civicrm.utils');
    if (!$cid) {
      $contact = $this->data['contact'][1];

      // 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) {
      $cid = $this
        ->createContact($contact);
      $this->billing_contact = $cid;
    }
    else {
      foreach ([
        'address',
        'email',
      ] as $loc) {
        $result = $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'][1]['id'] = $cid;
      $utils
        ->wf_civicrm_api('address', 'create', $address);
      $utils
        ->wf_civicrm_api('email', 'create', $email);
    }
    return $cid;
  }

  /**
   * Execute payment processor transaction
   * This happens during form validation and sets a form error if unsuccessful
   */
  private function submitLivePayment() {
    $contributionParams = $this
      ->contributionParams();
    $utils = \Drupal::service('webform_civicrm.utils');

    // 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 {
      $result = $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') {
    $utils = \Drupal::service('webform_civicrm.utils');
    $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 = $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 ($paymentType === 'deferred') {
      $APIAction = 'create';
    }
    $result = $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'])),
      ];
      if ($APIAction == 'transact') {

        // Now that we include Transact within webform_civicrm module it comes back flattened:
        $recurParams['invoice_id'] = $result['invoice_id'];
        $recurParams['payment_instrument_id'] = $result['payment_instrument_id'];
      }
      else {
        $recurParams['invoice_id'] = $result['values'][$result['id']]['invoice_id'];
        $recurParams['payment_instrument_id'] = $result['values'][$result['id']]['payment_instrument_id'];
      }
      $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) && $this->contributionIsPayLater) {
      $result = $this
        ->contributionRecur($params, 'deferred');
    }
    else {
      $result = \Drupal::service('webform_civicrm.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() {
    $utils = \Drupal::service('webform_civicrm.utils');
    $params = $this->data['contribution'][1]['contribution'][1];
    $processor_type = $utils
      ->wf_civicrm_api('payment_processor', 'getsingle', [
      'id' => $params['payment_processor_id'],
    ]);
    $paymentProcessor = \Civi\Payment\System::singleton()
      ->getById($params['payment_processor_id']);

    // 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)
    $contact = $utils
      ->wf_civicrm_api('contact', 'getsingle', [
      'id' => $this->ent['contact'][1]['id'],
    ]);
    $params += $contact;
    $params['contributionID'] = $params['id'] = $this->ent['contribution'][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'];
    $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')) {
      $paymentProcessor
        ->setSuccessUrl($this
        ->getIpnRedirectUrl('success'));
      $paymentProcessor
        ->setCancelUrl($this
        ->getIpnRedirectUrl('cancel'));
    }
    try {
      $paymentProcessor
        ->doPayment($params);
    } catch (\Civi\Payment\Exception\PaymentProcessorException $e) {
      drupal_set_message(ts('Payment approval failed with message: ') . $e
        ->getMessage(), 'error');
      \CRM_Utils_System::redirect($this
        ->getIpnRedirectUrl('cancel'));
    }
  }

  /**
   * @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;
    $params['contact_id'] = $this->ent['contact'][1]['id'];
    $params['total_amount'] = round($this->totalContribution, 2);
    $utils = \Drupal::service('webform_civicrm.utils');

    // 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 = $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;
      }
    }

    // Fix bug for testing.
    // @todo Pay Later causes issues as it returns `0`.
    if ($params['is_test'] == 1 && $params['payment_processor_id'] !== '0') {
      $liveProcessorName = $utils
        ->wf_civicrm_api('payment_processor', 'getvalue', [
        'id' => $params['payment_processor_id'],
        'return' => 'name',
      ]);

      // Lookup current domain for multisite support
      static $domain = 0;
      if (!$domain) {
        $domain = $utils
          ->wf_civicrm_api('domain', 'get', [
          'current_domain' => 1,
          'return' => 'id',
        ]);
        $domain = wf_crm_aval($domain, 'id', 1);
      }
      $params['payment_processor_id'] = $utils
        ->wf_civicrm_api('payment_processor', 'getvalue', [
        'return' => 'id',
        'name' => $liveProcessorName,
        'is_test' => 1,
        'domain_id' => $domain,
      ]);
    }
    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 = $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['honor_contact_id'], $params['honor_type_id']);
    return $params;
  }

  /**
   * Post-processing of contribution
   * This happens during form post-processing
   */
  private function processContribution() {
    $utils = \Drupal::service('webform_civicrm.utils');
    $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 = $utils
        ->wf_civicrm_api('OptionValue', 'getsingle', [
        'return' => "value",
        'option_group_id' => "soft_credit_type",
        'is_default' => 1,
      ]);
      foreach (array_filter($contribution['soft']) as $cid) {
        $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' => $default_soft_credit_type['value'],
        ]);
      }
    }

    // Save honoree
    // FIXME: these api params were deprecated in 4.5, should be switched to use soft-credits when we drop support for 4.4
    if (!empty($contribution['honor_contact_id']) && !empty($contribution['honor_type_id'])) {
      $utils
        ->wf_civicrm_api('contribution', 'create', [
        'id' => $id,
        'total_amount' => $contribution['total_amount'],
        'honor_contact_id' => $contribution['honor_contact_id'],
        'honor_type_id' => $contribution['honor_type_id'],
      ]);
    }
    $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'])) {
        continue;
      }
      if (empty($item['entity_id'])) {
        $item['entity_id'] = $id;
      }

      // tax integration
      if (empty($item['contribution_id'])) {
        $item['contribution_id'] = $id;
      }

      // Membership
      if (!empty($item['membership_id'])) {
        $item['entity_id'] = $item['membership_id'];
        $lineItemArray = $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'];
          }
        }
      }

      // Save the line_item
      $line_result = $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';
        $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) {
        list(, $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 $customOnly
   *   TRUE if only custom contact reference needs to be filled.
   * @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($customOnly = FALSE, $values = NULL, $depth = 0) {
    $order = [
      'ent',
      'c',
      'table',
      'n',
      'name',
    ];
    static $ent = '';
    static $c = '';
    static $table = '';
    static $n = '';
    $customName = NULL;
    if ($values === NULL) {
      $values = $this->data;
    }
    foreach ($values as $key => $val) {
      ${$order[$depth]} = $key;
      if ($depth < 4 && is_array($val)) {
        $this
          ->fillContactRefs($customOnly, $val, $depth + 1);
      }
      elseif ($depth == 4 && $val && wf_crm_aval($this->all_fields, "{$table}_{$name}:data_type") == 'ContactReference') {
        if ($customOnly && substr($name, 0, 6) != 'custom') {
          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 = $customOnly ? $ent : $table;
              $this->data[$ent][$c][$tableName][$n][$name][] = $this->ent['contact'][$v]['id'];
            }
          }
        }
        else {
          $createModeKey = 'civicrm_' . $c . '_contact_' . $n . '_' . $table . '_createmode';
          $multivaluesCreateMode = $this->data['config']['create_mode'][$createModeKey] ?? NULL;
          $cgMaxInstance = $this->all_sets[$table]['max_instances'] ?? 1;
          $key = $n;
          if (substr($name, 0, 6) == 'custom' && $cgMaxInstance > 1) {

            // Retrieve name for multi value custom fields.
            $customName = $this
              ->getNameForMultiValueFields($multivaluesCreateMode, $name, $table, $c, $n);
            $key = 1;
          }
          if (!empty($this->ent['contact'][$val]['id'])) {
            unset($this->data[$ent][$c][$table][$n][$name]);
            $tableName = substr($name, 0, 6) == 'custom' ? $ent : $table;
            $fld = $customName ?? $name;
            $this->data[$ent][$c][$tableName][$key][$fld] = $this->ent['contact'][$val]['id'];
          }
          elseif (substr($name, 0, 6) == 'custom') {
            $this->data[$ent][$c]['update_contact_ref'][$n][$name] = $val;
          }
        }
      }
    }
  }

  /**
   * Fill data array with submitted form values
   */
  private function fillDataFromSubmission() {
    $utils = \Drupal::service('webform_civicrm.utils');
    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,
      ]) {
        list(, $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)) {

          // Also remove the value from the webform submission
          $this
            ->submissionValue($fid, [
            NULL,
          ]);
          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);
          }

          // 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);
          }
        }
        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') {
          if (empty($val[0]) || !($val = $this
            ->saveDrupalFileToCivi($val[0]))) {

            // 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;
        }
        elseif ($field['type'] === '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 = $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') {
            $multivaluesCreateMode = NULL;

            // Handle 'Create mode' for multivalues custom fields of Contact entity.
            if ($ent === 'contact' && is_numeric(substr($name, 7))) {
              $createModeKey = 'civicrm_' . $c . '_contact_' . $n . '_' . $table . '_createmode';
              $multivaluesCreateMode = $this->data['config']['create_mode'][$createModeKey] ?? NULL;
              $cgMaxInstance = $this->all_sets[$table]['max_instances'] ?? 1;

              // Is this a multi-value custom field?
              if ($cgMaxInstance > 1) {
                $name = $this
                  ->getNameForMultiValueFields($multivaluesCreateMode, $name, $table, $c, $n);
                $n = 1;
              }
            }
            $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'])) {

          // Check to see if configured to Submit disabled field value(s)
          if ($checkSubmitDisabledSetting && $component['submit_disabled']) {
            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?
            $value = wf_crm_aval($this
              ->loadContact($c), "{$table}:{$n}:{$name}");
            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 {
      $data = $this->submission
        ->getData();
    }
    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) {
      $field = (array) $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
    $utils = \Drupal::service('webform_civicrm.utils');
    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 ($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 ($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;
  }

  /**
   * Retrieve name for multi-value custom fields.
   *
   * For edit mode, update the $name key to 'custom_<fieldID>_<existing_value_id>.
   *
   * If webform is configured to only "insert" new values,
   * modify the params in the format of custom_<fieldID>_-1, custom_<fieldID>_-2, etc.
   *
   * @param bool $multivaluesCreateMode
   * @param string $name
   * @param string $table
   * @param int $c
   * @param int $n
   *
   * @return string $name
   */
  private function getNameForMultiValueFields($multivaluesCreateMode, $name, $table, $c, $n) : string {

    // Multi value fields are already saved while creating the billing contact.
    if ($c == 1 && !empty($this->billing_contact)) {
      return '';
    }
    $existingValue = NULL;
    if (empty($multivaluesCreateMode) && !empty($this->existing_contacts[$c])) {
      $existingValue = $this
        ->getCustomData($this->existing_contacts[$c], NULL, FALSE)[$table] ?? NULL;
      if (!empty($existingValue) && is_array($existingValue)) {
        $existingValue = key(array_slice($existingValue, $n - 1, 1, TRUE));
        if (!empty($existingValue)) {
          return "{$name}_{$existingValue}";
        }
      }
    }

    // No existing value found, insert a new value.
    if (empty($existingValue)) {
      return "{$name}_-{$n}";
    }
    return $name;
  }

  /**
   * 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)) {

              //If this is an edit to an existing record, remove any matching keys that was previously set to 'create' new records.
              if (strpos($key, "{$refKey}_-") !== 0) {
                foreach ($updateParams as $k => $v) {
                  if (strpos($k, "{$refKey}_-") === 0) {
                    $skipKeys[] = $k;
                    unset($updateParams[$k]);
                  }
                }
              }
              $updateParams[$key] = $value;
            }
          }
        }
      }
    }
    if (count($updateParams) > 1) {
      \Drupal::service('webform_civicrm.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();
  }

}

Members

Namesort descending Modifiers Type Description Overrides
WebformCivicrmBase::$data protected property
WebformCivicrmBase::$enabled protected property
WebformCivicrmBase::$ent protected property
WebformCivicrmBase::$events protected property
WebformCivicrmBase::$line_items protected property
WebformCivicrmBase::$loadedContacts protected property
WebformCivicrmBase::$membership_types protected property
WebformCivicrmBase::$node protected property
WebformCivicrmBase::$settings protected property
WebformCivicrmBase::$_payment_processor private property
WebformCivicrmBase::$_tax_rate private property
WebformCivicrmBase::addPaymentJs function CiviCRM JS can't be attached to a drupal form so have to manually re-add this during validation
WebformCivicrmBase::add_user_select_field_placeholder protected function Add location_type_id = NULL for user-select fields for identification later
WebformCivicrmBase::findCaseForContact function Find a case matching criteria
WebformCivicrmBase::findContact protected function Find an existing contact based on matching criteria Used to populate a webform existing contact field
WebformCivicrmBase::findMemberships protected function Get memberships for a contact
WebformCivicrmBase::getAttachments public function FIXME: Use the api for this
WebformCivicrmBase::getComponent protected function Fetch a webform component given its civicrm field key
WebformCivicrmBase::getCustomData protected function Get custom data for an entity
WebformCivicrmBase::getData protected function
WebformCivicrmBase::getDefaults function Returns a default value for a component.
WebformCivicrmBase::getDrupalFileUrl function Fetch the public url of a file in the Drupal file system
WebformCivicrmBase::getExposedOptions protected function For a given field, find the options that are exposed to the webform.
WebformCivicrmBase::getFileInfo function Retrieve info needed for pre-filling a webform file field
WebformCivicrmBase::getMembershipTypeField protected function
WebformCivicrmBase::getQfKey public function Generate the quickform key needed to access a contribution form
WebformCivicrmBase::getRelationship protected function Fetch relationship for a pair of contacts
WebformCivicrmBase::handleRemainingValues protected function Put remaining values in 'user-select' fields
WebformCivicrmBase::loadBillingAddress protected function Load Billing Address for contact.
WebformCivicrmBase::loadContact protected function Fetch all relevant data for a given contact Used to load contacts for pre-filling a webform, and also to fill in a contact via ajax
WebformCivicrmBase::loadEvents protected function Fetch info and remaining spaces for events
WebformCivicrmBase::matchLocationTypes protected function Organize values according to location types
WebformCivicrmBase::matchWebsiteTypes protected function Organize values according to website types
WebformCivicrmBase::MULTIVALUE_FIELDSET_MODE_CREATE_OR_EDIT constant
WebformCivicrmBase::reorderByLocationType protected function Reorder returned results according to settings chosen in wf_civicrm backend
WebformCivicrmBase::saveDrupalFileToCivi public static function Copies a drupal file into the Civi file system
WebformCivicrmBase::__get function Magic method to retrieve otherwise inaccessible properties
WebformCivicrmPostProcess::$all_fields private property
WebformCivicrmPostProcess::$all_sets private property
WebformCivicrmPostProcess::$billing_params private property
WebformCivicrmPostProcess::$contributionIsIncomplete private property
WebformCivicrmPostProcess::$contributionIsPayLater private property
WebformCivicrmPostProcess::$crmValues private property
WebformCivicrmPostProcess::$database private property
WebformCivicrmPostProcess::$existing_contacts private property
WebformCivicrmPostProcess::$form private property
WebformCivicrmPostProcess::$form_state private property
WebformCivicrmPostProcess::$initialized protected property Static cache.
WebformCivicrmPostProcess::$multiPageDataLoaded private property
WebformCivicrmPostProcess::$rawValues private property
WebformCivicrmPostProcess::$shared_address private property
WebformCivicrmPostProcess::$submission private property
WebformCivicrmPostProcess::$totalContribution private property
WebformCivicrmPostProcess::$update private property
WebformCivicrmPostProcess::addOrRemoveMultivaluedData private function Handle adding/removing multivalued data for a contact/activity/etc. Currently used only for groups and tags, but written with expansion in mind.
WebformCivicrmPostProcess::BILLING_MODE_LIVE constant
WebformCivicrmPostProcess::contributionParams private function Format contribution params for transact/pay-later
WebformCivicrmPostProcess::contributionRecur private function Generate recurring contribution and transact the first one
WebformCivicrmPostProcess::createBillingContact private function Create contact 1 if not already existing (required by contribution.transact)
WebformCivicrmPostProcess::createContact private function Create a new contact
WebformCivicrmPostProcess::createDeferredPayment private function Create Pending (Pay Later) Contribution
WebformCivicrmPostProcess::expireRelationship private function End relationship for a pair of contacts
WebformCivicrmPostProcess::fillContactRefs private function Recursive function to fill ContactRef fields with contact IDs
WebformCivicrmPostProcess::fillDataFromSubmission private function Fill data array with submitted form values
WebformCivicrmPostProcess::fillHiddenContactFields private function Fill values for hidden ID & CS fields
WebformCivicrmPostProcess::findDuplicateContact private function Search for an existing contact using configured deupe rule
WebformCivicrmPostProcess::formatSubmission private function Formats submission data as expected by the schema
WebformCivicrmPostProcess::formatSubmissionDetails private function Process the submission into the details of the activity.
WebformCivicrmPostProcess::getExistingContactIds private function Fetch contact ids from "existing contact" fields
WebformCivicrmPostProcess::getIpnRedirectUrl public function
WebformCivicrmPostProcess::getNameForMultiValueFields private function Retrieve name for multi-value custom fields.
WebformCivicrmPostProcess::getPaymentInstrument private function Retrieve payment instrument.
WebformCivicrmPostProcess::getReceiptParams private function Build params for contribution receipt.
WebformCivicrmPostProcess::handleEntityTags public function Handle adding/updating tags for entities (cases, activity)
WebformCivicrmPostProcess::initialize function Overrides WebformCivicrmPostProcessInterface::initialize
WebformCivicrmPostProcess::isContactEmpty private function Check if at least one required field was filled for a contact
WebformCivicrmPostProcess::isFieldHiddenByExistingContactSettings private function Test whether a field has been hidden due to existing contact settings
WebformCivicrmPostProcess::isLivePaymentProcessor private function
WebformCivicrmPostProcess::isPaymentPage private function Are billing fields exposed to this webform page?
WebformCivicrmPostProcess::loadMultiPageData private function 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
WebformCivicrmPostProcess::locationIsEmpty private function Test if any relevant data has been entered for a location
WebformCivicrmPostProcess::modifyWebformSubmissionData protected function 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.
WebformCivicrmPostProcess::postSave public function Process webform submission after it is has been saved. Called by the following hooks: Overrides WebformCivicrmPostProcessInterface::postSave
WebformCivicrmPostProcess::preSave public function Process webform submission when it is about to be saved. Called by the following hook: Overrides WebformCivicrmPostProcessInterface::preSave
WebformCivicrmPostProcess::processActivities private function Save activity data
WebformCivicrmPostProcess::processAttachments private function
WebformCivicrmPostProcess::processCases private function Save case data
WebformCivicrmPostProcess::processContribution private function Post-processing of contribution This happens during form post-processing
WebformCivicrmPostProcess::processGrants private function Save grants
WebformCivicrmPostProcess::processMemberships private function Process memberships for a contact Called during webform submission
WebformCivicrmPostProcess::processParticipants private function Process event participation for a contact
WebformCivicrmPostProcess::processRelationship private function Add/update relationship for a pair of contacts
WebformCivicrmPostProcess::processSharedAddresses private function Process shared addresses
WebformCivicrmPostProcess::reorderLocationValues protected function Reorder submitted location values according to existing location values
WebformCivicrmPostProcess::saveContactLocation private function Save location data for a contact
WebformCivicrmPostProcess::saveContactRefs private function Save custom contact reference created during the current submission of the webform.
WebformCivicrmPostProcess::saveCurrentEmployer function Save current employer for a contact
WebformCivicrmPostProcess::saveGroupsAndTags private function Save groups and tags for a contact
WebformCivicrmPostProcess::saveRelationships private function Save relationships for a contact
WebformCivicrmPostProcess::sendReceipt private function Send receipt
WebformCivicrmPostProcess::setLoggingContact public function Identifies contact 1 as acting user for CiviCRM's advanced logging
WebformCivicrmPostProcess::setUpdateParam private function If this is an update op, set param for drupal_write_record()
WebformCivicrmPostProcess::submissionValue protected function Get or set a value from a webform submission Always use \Drupal\webform\WebformSubmissionInterface $webform_submission [more reliable according to JR]
WebformCivicrmPostProcess::submitIPNPayment private function Call IPN payment processor to redirect to payment site
WebformCivicrmPostProcess::submitLivePayment private function Execute payment processor transaction This happens during form validation and sets a form error if unsuccessful
WebformCivicrmPostProcess::tallyLineItems private function Calculate line-items for this webform submission
WebformCivicrmPostProcess::unsetEmptyValueIndexes private function
WebformCivicrmPostProcess::unsetError private function Clears an error against a form element. Used to disable validation when this module hides a field
WebformCivicrmPostProcess::updateContact private function Update a contact
WebformCivicrmPostProcess::validate public function Called after a webform is submitted Or, for a multipage form, called after each page Overrides WebformCivicrmPostProcessInterface::validate
WebformCivicrmPostProcess::validateBillingFields private function Normalize and validate billing input
WebformCivicrmPostProcess::validateParticipants private function Validate event participants and add line items
WebformCivicrmPostProcess::validateThisPage private function Recursive validation callback for webform page submission
WebformCivicrmPostProcess::verifyExistingContact private function Ensure we have a valid contact id in a contact ref field