AlexaCRM / dynamics-webapi-toolkit

Dynamics 365 Web API Toolkit for PHP
MIT License
75 stars 58 forks source link

Ability to associate Activity Parties #16

Open shaunjc opened 4 years ago

shaunjc commented 4 years ago

Native support for Activity parties does not appear to be a core part of the toolkit functionality. The serialization helper seems to strip these fields as they do not show in the returned metadata with the current implementation.

As an example, attempting to create a campaignresponse with a matching customer does not work like the standard way to update and associate records.

$contact_ref = new \AlexaCRM\Xrm\EntityReference( 'contact', $contactid );

// $campaignresponse['customer'] is stripped out during $serializer->serializeEntity( $campaignresponse ); A 400 Bad Request error would be thrown otherwise.
$campaignresponse['customer'] = $contact_ref;

// A different 400 Bad Request error is thrown for attempting to associate using this action.
$client()->Associate(
    'campaignresponse',
    $campaignresponse->Id,
    new \AlexaCRM\Xrm\Relationship( 'contact_campaigncesponses' ),
    [ $contact_ref ]
);

// Same for the reverse association - a similar error will be thrown.
$client()->Associate(
    'contact',
    $contactId,
    new \AlexaCRM\Xrm\Relationship( 'contact_campaigncesponses' ),
    [ new \AlexaCRM\Xrm\EntityReference( 'campaignresponse', $campaignresponse->Id )]
);

// It is possible to set this by serializing the data, adjusting the payload and using the OData Client.
$serializer = new \AlexaCRM\WebAPI\SerializationHelper( $client->getClient() );
// Creating the ActivityParty as an Entity helps with serialization.
$activityparty = new \AlexaCRM\Xrm\Entity( 'activityparty' );
$activityparty['partyid'] = $contact_ref;
$activityparty['participationtypemask'] = 11;
// Activity Party and campaign response are serialized and attached to each other.
$translatedData = $serializer->serializeEntity( $campaignresponse );
$translatedData['campaignresponse_activity_parties'] = array( $serializer->serializeEntity( $activityparty ) );
// Using the OData\Client gets around serialized data being lost.
$client->getClient()->update( $client->getClient()->getMetadata()->getEntitySetName( 'campaignresponse' ), $translatedData );

A new class for Activity Party entity types which encode themselves appropriately and possibly match the correct parts of the metadata, would help simplify the process of linking data.

// Potential new class
class ActivityParty implements \ArrayAccess {
    public $Entities = [];
    public function __construct( $entityReference, $type ) {
    }
}
// Possible example usage
$activityparty = new ActivityParty( $contact_ref, ActivityParty::TYPE_CUSTOMER );
$campaignresponse['campaignresponse_activity_parties'] = $activityparty;
wizardist commented 4 years ago

Hi @shaunjc

Could you provide error messages received from Web API please? Thanks

shaunjc commented 4 years ago

Hi @wizardist,

Each of these throw a 400 Bad Request when attempting to POST the following data.

$translatedData['customer@odata.bind'] = "/contacts({$contactid})";

An undeclared property 'customer' which only has property annotations in the payload but no property value was found in the payload. In OData, only declared navigation properties and declared named streams can be represented as properties without values.

$translatedData['customer'] = array( $customer_ref );

An error occurred while validating input parameters: Microsoft.OData.ODataException: Does not support untyped value in non-open type.

$client()->Associate(
    'campaignresponse',
    $campaignresponse->Id,
    new \AlexaCRM\Xrm\Relationship( 'contact_campaigncesponses' ),
    [ $contact_ref ]
);

The URI segment '$ref' is invalid after the segment 'customers'.

$client()->Associate(
    'contact',
    $contactId,
    new \AlexaCRM\Xrm\Relationship( 'contact_campaigncesponses' ),
    [ new \AlexaCRM\Xrm\EntityReference( 'campaignresponse', $campaignresponse->Id )]
);

The URI segment '$ref' is invalid after the segment 'contact_campaignresponses'.

wizardist commented 4 years ago

Hi @shaunjc

One thing I want to point out is that relationship schema names are case-sensitive and sensitive to spelling mistakes. And CDS / Dynamics 365 have a contact_CampaignResponses relationship. And only contact entity has this relationship. Its partner in campaignresponse is regardingobjectid_contact_campaignresponse.

customer and customers are party lists, and they are not exposed to Web API. I'm afraid you will have to create activityparty records with the corresponding participationtypemask, in this case it's 11 (Customer).

shaunjc commented 4 years ago

Hi @wizardist,

That does seem odd as the only items that I've seen that are in PascalCase are the entity properties. All other data I get from the API (and in turn submit back to it), such as each of the attribute keys and entity names, are all in lowercase.

The case sensitivity here probably explains why the Contact relation didn't work, and while I did find the regardingobjectid_contact_campaignresponse, I didn't expect that to be the relationship name.

Regardless, the last paragraph in that first code block was what I implemented to get the data saving in the first place.

// It is possible to set this by serializing the data, adjusting the payload and using the OData Client. $serializer = new \AlexaCRM\WebAPI\SerializationHelper( $client->getClient() ); // Creating the ActivityParty as an Entity helps with serialization. $activityparty = new \AlexaCRM\Xrm\Entity( 'activityparty' ); $activityparty['partyid'] = $contact_ref; $activityparty['participationtypemask'] = 11; // Activity Party and campaign response are serialized and attached to each other. $translatedData = $serializer->serializeEntity( $campaignresponse ); $translatedData['campaignresponse_activity_parties'] = array( $serializer->serializeEntity( $activityparty ) ); // Using the OData\Client gets around serialized data being lost. $client->getClient()->update( $client->getClient()->getMetadata()->getEntitySetName( 'campaignresponse' ), $translatedData );

As you can see I did use the participationtypemask of 11, and it works so long as I update the serialized data and access the OData client.

I still think that finding some way to expose / leverage Party Lists through the Web API would be a beneficial feature to have, however I understand if you would prefer to close this feature request.

wizardist commented 4 years ago

@shaunjc What you're trying to do here is a deep insert, and the toolkit indeed does not support that yet. But it is a valid feature request. 👍

As for exposing party lists, we would require to look up organization metadata instead of OData metadata, which is a different beast. But it is doable in theory ;)

shaunjc commented 4 years ago

On the topic of deep retrievals, the easiest way I found to access and manage associations was to access the ODataClient.

$_accounts = $client->getClient()->getRecord(
    $client->getClient()->getMetadata()->getEntitySetName( 'list' ),
    $list->listid,
    [
        'Expand' => 'listaccount_association',
        'Select' => [ 'listaccount_association' ]
    ]
);
foreach ( $_accounts->listaccount_association as $_account ) {
    $account = $serializer->deserializeEntity(
        $_account,
        new AlexaCRM\Xrm\EntityReference( 'account', $_account->accountid )
    );
   // Do stuff here.
}

Adding a new optional parameter to Client::Retrieve to update $options before they are sent to ODataClient::getRecord may be a suitable solution.