Hubspot APIを利用して、取引作成と既存コンタクトへの関連付けをするJob

コンタクトとの販売やり取りを管理する取引はとても便利

参考: [取引を作成する|Hubspot ナレッジベース](https://knowledge.hubspot.com/ja/crm-deals/create-deals)

今回の利用ケースでは
コンタクトから利用申し込みがあった時点で、販売やり取りが開始されるので
申し込みをするというイベントの後、自動で取引を作成する処理が必要になりました

HubspootのそれぞれのAPIを順番に呼んでいくと言うだけなのですが
備忘録のために書いていきます


## 大まかな流れ
![スクリーンショット 2023-03-02 8.25.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108154/27525734-6a21-2aea-1d8f-d39db23b8643.png)

今回はLaravel上でJobを実装し
Hubspot APIの実行をしています

## 事前準備

- 取引のパイプラインを用意する(デフォルト利用でも可)
    - それぞれパイプラインにはIDがあるのでメモっておく
- パイプラインにステージを用意する(デフォルト利用でも可)
    - それぞれステージにはIDがあるのでメモっておく

## 実行コード

~~~php
<?php

namespace App\Jobs;

use App\Models\User;
use Exception;
use HubSpot\Factory;
use HubSpot\Discovery\Discovery;
use HubSpot\Client\Crm\Contacts\ApiException as ContactsApiException;
use HubSpot\Client\Crm\Contacts\Model\Filter;
use HubSpot\Client\Crm\Contacts\Model\FilterGroup;
use HubSpot\Client\Crm\Contacts\Model\PublicObjectSearchRequest;
use HubSpot\Client\Crm\Deals\Model\AssociationSpec;
use HubSpot\Client\Crm\Deals\Model\SimplePublicObjectInput;
use HubSpot\Client\Crm\Deals\ApiException as DealsApiException;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Redis\LimiterTimeoutException;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Throwable;


class HubSpotCreateDealsAssociationsContact implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    const DETAILS_STAGE_ID = [
        'local' => xxxxxx,
        'staging' => xxxxx,
        'production' => xxxxx,
    ]; // ステージIDを記載

    const DETAILS_PIPELINE_ID = [
        'local' => xxxxx,
        'staging' => xxxxx,
        'production' => xxxx,
    ]; // パイプラインID

    protected array $userProperties;

    /**
     * 最大試行回数
     *
     * @var int
     */
    public int $tries = 3;

    /**
     * @param User $user
     */
    public function __construct(
        User $user
    ) {
        $this->userProperties = [
            'email' => $user->email,
            'name' => $user->name,
        ];
    }

    /**
     * Execute the job.
     *
     * @return void
     * @throws LimiterTimeoutException
     */
    public function handle(): void
    {
            try {
                $hubspot = Factory::createWithAccessToken(config('services.hubspot.access_token'));
                // 取引の作成
                $apiResponseForCreateDeals = $this->createDeals($hubspot);
                if (!$apiResponseForCreateDeals->getId()) {
                    throw new Exception('取引の作成に失敗しました');
                }
                // コンタクトと紐付け
                // 紐付け先対象のコンタクト取得
                $contactId = $this->getContactInfo($hubspot);
                if ($contactId == null) {
                    throw new Exception('対象のcontact IDが見つかりませんでした');
                }
                $this->associationsContact($hubspot, $apiResponseForCreateDeals, $contactId);
            } catch (ContactsApiException $e) {
                Log::error("contacts()->searchApi()->doSearch() ". $e->getMessage());
            } catch (DealsApiException $e) {
                Log::error( "deals()->associationsApi()->create() ". $e->getMessage());
            } catch (Exception $e) {
                Log::error("HubSpotCreateDealsAssociationsContact Exception: ". $e->getMessage());
            }
    }

    /**
     * The job failed to process.
     *
     * @param  Throwable $e
     * @return void
     */
    public function failed(Throwable $e): void
    {
        Log::error(sprintf("<!here>\n %s ジョブの実行に失敗しました。\n :no_entry_sign: %s", config('app.env'), $e->getMessage()));
    }

    /**
     * 取引の作成
     *
     * @param Discovery $hubspot
     * @return SimplePublicObject|Error
     * @throws \HubSpot\Client\Crm\Deals\ApiException
     * @throws Exception
     */
    private function createDeals(Discovery $hubspot): SimplePublicObject|Error
    {
        // 取引の作成
        $dealsProperties = [
            'dealname' => '任意のタイトル',
            'dealstage' => self::DETAILS_STAGE_ID[config('app.env')],
            'pipeline' => self::DETAILS_PIPELINE[config('app.env')],
            // 取引のプロパティ値で更新したいものを書く
            // 'プロパティ固有ID' => 値
        ];

        $simplePublicObjectInput = new SimplePublicObjectInput([
            'properties' => $dealsProperties,
        ]);
        $apiResponseForCreateDeals = $hubspot->crm()->deals()->basicApi()->create($simplePublicObjectInput);
        Log::info('create Hubspot Details ID: ' . $apiResponseForCreateDeals->getId());

        return $apiResponseForCreateDeals;
    }

    /**
     * コンタクトの取得
     *
     * @param Discovery $hubspot
     * @return int|null
     * @throws ApiException
     */
    private function getContactInfo(Discovery $hubspot): ?int
    {
        $filter = new Filter();
        // ここではメールでコンタクトの取得を行います
        $filter
            ->setOperator('EQ')
            ->setPropertyName('email')
            ->setValue($this->userProperties['email']);

        $filterGroup = new FilterGroup();
        $filterGroup->setFilters([$filter]);

        $searchRequest = new PublicObjectSearchRequest();
        $searchRequest->setFilterGroups([$filterGroup]);

        // Get specific properties
        $searchRequest->setProperties(['email']);

        // @var CollectionResponseWithTotalSimplePublicObject $contactsPage
        $contactsPage = $hubspot->crm()->contacts()->searchApi()->doSearch($searchRequest);
        return $contactsPage['results'][0]['id'] ?? null;
    }

    /**
     * コンタクトと取引の紐付け
     *
     * @param Discovery $hubspot
     * @param SimplePublicObject $apiResponseForCreateDeals
     * @param int $contactId
     * @return void
     * @throws DealsApiException
     */
    private function associationsContact(
        Discovery $hubspot,
        SimplePublicObject $apiResponseForCreateDeals,
        int $contactId
    ): void {
        $associationSpec = new AssociationSpec([
            'association_category' => 'HUBSPOT_DEFINED',
            'association_type_id' => 3
        ]);

        $hubspot->crm()->deals()->associationsApi()->create(
            $apiResponseForCreateDeals->getId(),
            'Contact',
            $contactId,
            [$associationSpec]
        );
        Log::info('Hubspot Details Associations Contact');
    }
}
~~~

呼び出し元はお決まりのこんな感じで

~~~php
HubSpotCreateDealsAssociationsContact::dispatch($user);
~~~

## 実装にあたり参照したもの
APIのコールはhubspot公式のライブラリーを通して行なっています
これ: [hubspot/api-client](https://github.com/HubSpot/hubspot-api-php)

APIの仕様についてはこれまた公式ドキュメントを参照してます
[HubSpot API Docs](https://developers.hubspot.com/docs/api/overview)



### 関連付けのIDについて
associationsApi ( /crm/v4/objects/deals/{dealId}/associations/{toObjectType}/{toObjectId} )
のリクエストに使うAssociationSpecの引数ですが、下記のURLから関連付け先のものを参照しました

https://legacydocs.hubspot.com/docs/methods/crm-associations/crm-associations-overview
今回だと取引からコンタクトへの紐付けをしたので
Deal to contact： 3　となります

### 取引の作成と関連付けは一気にできないのか

わかる。
APIコール２回してて無駄っぽいので改善ポイント


# 最後に
取引の作成とコンタクトへの関連付けのコードの紹介をしました。
基本的には[HubSpot API Docs](https://developers.hubspot.com/docs/api/overview)に沿ってやっているだけですが、
関連付け処理で引数に迷うことがあったので、備忘録として記しました。

---
弊社では、一緒に働いてくれるエンジニアを募集しています。
エンジニアサイドからHubspotについて語れる方がいたらめっちゃ嬉しいです

↓そんな感じで興味のある方はこちらから↓
[Wantedly - スタジオアンビルト株式会社](https://www.wantedly.com/companies/studiounbuilt)

こんなWebサービスを作っています。
・[マドリー](https://madree.jp/)
・[Studio Unbuilt](https://www.studiounbuilt.com/)