APIv4 - Allow write to contact primary and billing locations
authorColeman Watts <coleman@civicrm.org>
Fri, 8 Jul 2022 12:51:42 +0000 (08:51 -0400)
committerColeman Watts <coleman@civicrm.org>
Tue, 2 Aug 2022 20:00:29 +0000 (16:00 -0400)
Provides symmetry with get operations, allowing email, phone, address & im to be
both read and written to within the contact api.

Civi/Api4/Action/Contact/ContactSaveTrait.php
Civi/Api4/Generic/Traits/DAOActionTrait.php
tests/phpunit/api/v4/Action/NullValueTest.php
tests/phpunit/api/v4/Entity/ContactJoinTest.php

index 67a9cca550738dbdf0f92aaa69040499c21acdf3..3fcc920a254b5a80556be5ec9deb38a7844e425e 100644 (file)
@@ -12,6 +12,9 @@
 
 namespace Civi\Api4\Action\Contact;
 
+use Civi\Api4\Utils\CoreUtil;
+use Civi\Api4\Utils\FormattingUtil;
+
 /**
  * Code shared by Contact create/update/save actions
  */
@@ -40,7 +43,52 @@ trait ContactSaveTrait {
         }
       }
     }
-    return parent::write($items);
+    $saved = parent::write($items);
+    foreach ($items as $index => $item) {
+      self::saveLocations($item, $saved[$index]);
+    }
+    return $saved;
+  }
+
+  /**
+   * @param array $params
+   * @param \CRM_Contact_DAO_Contact $contact
+   */
+  protected function saveLocations(array $params, $contact) {
+    foreach (['Address', 'Email', 'Phone', 'IM'] as $entity) {
+      foreach (['primary', 'billing'] as $type) {
+        $prefix = strtolower($entity) . '_' . $type . '.';
+        $item = FormattingUtil::filterByPrefix($params, $prefix . '*', '*');
+        // Not allowed to update by id or alter primary or billing flags
+        unset($item['id'], $item['is_primary'], $item['is_billing']);
+        if ($item) {
+          $labelField = CoreUtil::getInfoItem($entity, 'label_field');
+          // If NULL was given for the main field (e.g. `email`) then delete the record
+          if ($labelField && array_key_exists($labelField, $item) && is_null($item[$labelField])) {
+            civicrm_api4($entity, 'delete', [
+              'checkPermissions' => FALSE,
+              'where' => [
+                ['contact_id', '=', $contact->id],
+                ["is_$type", '=', TRUE],
+              ],
+            ]);
+          }
+          else {
+            $item['contact_id'] = $contact->id;
+            $item["is_$type"] = TRUE;
+            $saved = civicrm_api4($entity, 'save', [
+              'checkPermissions' => FALSE,
+              'records' => [$item],
+              'match' => ['contact_id', "is_$type"],
+            ])->first();
+            foreach ($saved as $key => $value) {
+              $key = $prefix . $key;
+              $contact->$key = $value;
+            }
+          }
+        }
+      }
+    }
   }
 
 }
index d8e511e70009b176a46a098149d9be03c5bace12..94e0d7af21eecc39bd64f38a0fab88a6da490f1b 100644 (file)
@@ -52,13 +52,15 @@ trait DAOActionTrait {
    * @return array
    */
   public function baoToArray($bao, $input) {
-    $allFields = array_column($bao->fields(), 'name');
+    $entityFields = array_column($bao->fields(), 'name');
+    $inputFields = array_map(function($key) {
+      return explode(':', $key)[0];
+    }, array_keys($input));
+    $combinedFields = array_unique(array_merge($entityFields, $inputFields));
     if (!empty($this->reload)) {
-      $inputFields = $allFields;
       $bao->find(TRUE);
     }
     else {
-      $inputFields = array_keys($input);
       // Convert 'null' input to true null
       foreach ($inputFields as $key) {
         if (($bao->$key ?? NULL) === 'null') {
@@ -67,8 +69,8 @@ trait DAOActionTrait {
       }
     }
     $values = [];
-    foreach ($allFields as $field) {
-      if (isset($bao->$field) || in_array($field, $inputFields)) {
+    foreach ($combinedFields as $field) {
+      if (isset($bao->$field) || in_array($field, $inputFields) || (!empty($this->reload) && in_array($field, $entityFields))) {
         $values[$field] = $bao->$field ?? NULL;
       }
     }
index 04ea355c47bea2d4b1d3884ca52d8925f770a25c..8dd0449f5afac78486495483e1f0457b49bd4464 100644 (file)
@@ -29,10 +29,10 @@ use Civi\Test\TransactionalInterface;
  */
 class NullValueTest extends Api4TestBase implements TransactionalInterface {
 
-  public function setUpHeadless() {
+  public function setUp(): void {
     $format = '{contact.first_name}{ }{contact.last_name}';
     \Civi::settings()->set('display_name_format', $format);
-    return parent::setUpHeadless();
+    parent::setUp();
   }
 
   public function testStringNull() {
index 40fadf38e0af0f2a6452e1538fa20e1da98a1dba..9f7d026e12976e9af2fd9fe98e8018ea5887af78 100644 (file)
 
 namespace api\v4\Entity;
 
+use Civi\Api4\Address;
 use Civi\Api4\Contact;
+use Civi\Api4\Email;
 use Civi\Api4\OptionValue;
 use api\v4\Api4TestBase;
+use Civi\Api4\Phone;
 
 /**
  * @group headless
@@ -101,4 +104,56 @@ class ContactJoinTest extends Api4TestBase {
     $this->assertEquals($labels, $fetchedContact['preferred_communication_method:label']);
   }
 
+  public function testCreateWithPrimaryAndBilling() {
+    $contact = $this->createTestRecord('Contact', [
+      'email_primary.email' => 'a@test.com',
+      'email_billing.email' => 'b@test.com',
+      'address_billing.city' => 'Hello',
+      'address_billing.state_province_id:abbr' => 'AK',
+      'address_billing.country_id:abbr' => 'USA',
+    ]);
+    $addr = Address::get(FALSE)
+      ->addWhere('contact_id', '=', $contact['id'])
+      ->execute();
+    $this->assertCount(1, $addr);
+    $this->assertEquals('Hello', $contact['address_billing.city']);
+    $this->assertEquals(1228, $contact['address_billing.country_id']);
+    $emails = Email::get(FALSE)
+      ->addWhere('contact_id', '=', $contact['id'])
+      ->execute();
+    $this->assertCount(2, $emails);
+    $this->assertEquals('a@test.com', $contact['email_primary.email']);
+    $this->assertEquals('b@test.com', $contact['email_billing.email']);
+  }
+
+  public function testUpdateDeletePrimaryAndBilling() {
+    $contact = $this->createTestRecord('Contact', [
+      'phone_primary.phone' => '12345',
+      'phone_billing.phone' => '54321',
+    ]);
+    Contact::update(FALSE)
+      ->addValue('id', $contact['id'])
+      // Delete primary phone, update billing phone
+      ->addValue('phone_primary.phone', NULL)
+      ->addValue('phone_billing.phone', 99999)
+      ->execute();
+    $phone = Phone::get(FALSE)
+      ->addWhere('contact_id', '=', $contact['id'])
+      ->execute()
+      ->single();
+    $this->assertEquals('99999', $phone['phone']);
+    $this->assertTrue($phone['is_billing']);
+    // Contact only has one phone now, so it should be auto-set to primary
+    $this->assertTrue($phone['is_primary']);
+
+    $get = Contact::get(FALSE)
+      ->addWhere('id', '=', $contact['id'])
+      ->addSelect('phone_primary.*')
+      ->addSelect('phone_billing.*')
+      ->execute()->single();
+    $this->assertEquals('99999', $get['phone_primary.phone']);
+    $this->assertEquals('99999', $get['phone_billing.phone']);
+    $this->assertEquals($get['phone_primary.id'], $get['phone_billing.id']);
+  }
+
 }