From 88ef1525255a2d2232b459f2ae040f67d65f1b8e Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Fri, 8 Jul 2022 08:51:42 -0400 Subject: [PATCH] APIv4 - Allow write to contact primary and billing locations 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 | 50 ++++++++++++++++- Civi/Api4/Generic/Traits/DAOActionTrait.php | 12 ++-- tests/phpunit/api/v4/Action/NullValueTest.php | 4 +- .../phpunit/api/v4/Entity/ContactJoinTest.php | 55 +++++++++++++++++++ 4 files changed, 113 insertions(+), 8 deletions(-) diff --git a/Civi/Api4/Action/Contact/ContactSaveTrait.php b/Civi/Api4/Action/Contact/ContactSaveTrait.php index 67a9cca550..3fcc920a25 100644 --- a/Civi/Api4/Action/Contact/ContactSaveTrait.php +++ b/Civi/Api4/Action/Contact/ContactSaveTrait.php @@ -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; + } + } + } + } + } } } diff --git a/Civi/Api4/Generic/Traits/DAOActionTrait.php b/Civi/Api4/Generic/Traits/DAOActionTrait.php index d8e511e700..94e0d7af21 100644 --- a/Civi/Api4/Generic/Traits/DAOActionTrait.php +++ b/Civi/Api4/Generic/Traits/DAOActionTrait.php @@ -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; } } diff --git a/tests/phpunit/api/v4/Action/NullValueTest.php b/tests/phpunit/api/v4/Action/NullValueTest.php index 04ea355c47..8dd0449f5a 100644 --- a/tests/phpunit/api/v4/Action/NullValueTest.php +++ b/tests/phpunit/api/v4/Action/NullValueTest.php @@ -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() { diff --git a/tests/phpunit/api/v4/Entity/ContactJoinTest.php b/tests/phpunit/api/v4/Entity/ContactJoinTest.php index 40fadf38e0..9f7d026e12 100644 --- a/tests/phpunit/api/v4/Entity/ContactJoinTest.php +++ b/tests/phpunit/api/v4/Entity/ContactJoinTest.php @@ -19,9 +19,12 @@ 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']); + } + } -- 2.25.1