SalesforceからinvoiceAgentにファイル保存

概要

2022年1月より施行された改正電子帳簿保存法に対応するため、invoiceAgentで電子ファイルを保存することになりました。
ユーザ操作はSalesforceから行いinvoiceAgentに適切に電子ファイルを保存する仕組みの構築をどのように行ったのか、直面した問題や苦心した点を紹介いたします。


『Salesforce』とは、クラウドベースのCRM・SFAサービスを提供しているアメリカの企業名であり、サービスの総称です。
『invoiceAgent』とは、企業間で流通する帳票の最適化を実現し、ビジネスを加速させる電子帳票プラットフォームです。その中の、あらゆる帳票の仕分けから保管、検索、他システムとの連携も可能な文書管理ソリューションである『invoiceAgent 文書管理』を使用しています。

ソリューション

ソリューション概要

  • ファイル保存・ダウンロードの操作をSalesforceから実施し、利用者はinvoiceAgentを直接使用しない
  • Salesforceレコードの情報をinvoiceAgentのカスタムプロパティとして追加しファイルを保存

ソリューションの背景

電子帳簿保存法の保存要件に対応した、タイムスタンプの付与、高度な検索機能、証跡管理が可能な『invoiceAgent』を文書の保存先とすることになりました。
しかし、運用ルールに則った保存フォルダの作成や検索のためのカスタムプロパティの設定を、すべてのユーザが抜けなく行うことができるかに不安がありました。
そこで、基幹システムとして使用しているSalesforceを使用し、操作は簡単・入力は最小限の画面でinvoiceAgentにファイルを保存するシステムを構築することになりました。

構築したシステムの要件

  • SalesforceからinvoiceAgentにファイルを保存・ダウンロードする
  • invoiceAgentで保存フォルダを作成し、適切な場所にファイルを保存する
  • invoiceAgentで検索のためのカスタムプロパティをファイルの情報として設定する
  • 画面はシンプルにし、Salesforceレコード情報をカスタムプロパティの値に使用する
  • invoiceAgentに保存するファイルはSalesforceレコードと紐づけて管理する
  • 使用可能なSalesforceオブジェクトは固定せず汎用的な仕組みとする
  • カスタムオブジェクトを追加せずに機能を実現する

システム構成

Apexガバナ制限によりファイルサイズが大きいものを処理で扱えなかったため、ファイル保存・ダウンロードはGoogle Cloud 上に構築した専用の仲介サービスを経由して行いました。
『CIO』とは、当社のSalesforce環境の名称です。

システム構成

invoiceAgentとシステムの関係

No機能対応
1 タイムスタンプ付与 invoiceAgent
2 検索機能 invoiceAgent + Salesforce
3 履歴管理 invoiceAgent
4 ユーザの操作画面 Salesforce
5 監査向けの操作画面 invoiceAgent

システムの機能

  • ファイル保存・更新
  • ファイル一覧
  • ファイルダウンロード
  • ファイル削除
  • カスタムプロパティ表示
  • ファイル簡易検索
  • Salesforceレコード項目値とinvoiceAgentカスタムプロパティ値の同期

技術的なポイント

システム構築にあたり、直面した問題や苦心した点を紹介いたします。

Apexガバナ制限でファイルアップロードはGoogle Cloud Platformを使用

Salesforceで込み入ったシステムを開発すると直面するのがApexガバナ制限です。
今回は「ヒープの合計サイズ : 6MB」および「String length exceeds maximum: 6000000」 に引っかかりました。
当初は、invoiceAgentのAPI「archives_v5」をコールし、Salesforceに一度上げたファイルを文字列変換・加工し、最終的にBlobとして渡すように構築していました。しかしファイルサイズが大きくなると制約に引っかかりエラーとなりました。
Apex処理の中でファイルを扱うことは、ファイルをヒープメモリに展開することと同義になり、大きなファイルを扱うことができません。6MBは扱うファイルとしては小さすぎ実運用はできません。
そこで、解決方法を外に求め、『Google Cloud Functions』を使用してアーカイブすることにしました。
Google Cloud Functionsを使用したのは、当社ではGCPの導入支援も実施しており、既に当社SalesforceとGCPの連携を他の要件で実施していたからです。
なお、当社導入支援はこちらのページになります。興味ある方は是非お問い合わせください。
https://www.nddhq.co.jp/technology/it-infrastructure/gcp.html

仕組みとしては以下のように構築しました。

  1. 【SFDC】LWCで「lightning-file-upload」コンポーネントを使用してSalesforceにファイルアップロード
  2. 【SFDC】invoiceAgentに保存するフォルダのパスやカスタムプロパティ値をApex処理の中で組み立て
  3. 【SFDC】Google Cloud Functionsを前項で組み立てた情報とSalesforceFileへのリンクアドレスを渡して呼出し
  4. 【GCP 】Salesforceからリンクアドレスを元にファイルを取得
  5. 【GCP 】invoiceAgentのAPI「archives_v5」をコール、SFDCから指定したフォルダ・カスタムプロパティでファイルをアーカイブ
  6. 【GCP】処理結果をレスポンスとして返す
  7. 【SFDC】Google Cloud Functionsの処理結果を受取り、SalesforceFileを削除
SalesforceとGoogle Cloud FunctionsとinvoiceAgentの連携イメージ図
SalesforceとGoogle Cloud FunctionsとinvoiceAgentの連携イメージ図

セキュリティに引っかかりファイルダウンロードもGoogle Cloud Platformを使用

Apex処理の中でファイルを扱えないことはわかっていたため、LWCから直接ファイルをダウンロードする方法を検討しました。
JavaScriptから直接APIを実行できないかと考えましたが、レスポンスヘッダー「Access-Control-Allow-Origin」が設定されていないため、クロスサイトスクリプトのセキュリティに引っかかり実行することができませんでした。
invoiceAgentの設定として許可したドメインを設定し、APIのレスポンスヘッダー「Access-Control-Allow-Origin」を返すことができないかをサポート問合せしましたができないということで、ダウンロードもGoogle Cloud Functionsを使用することにしました。

仕組みとしては以下のように構築しました。

  1. 【SFDC】GCPへのアクセスURLを組み立て(Apex処理)
  2. 【SFDC】リンク要素を前項のURLで用意し、リンククリック実行し、GCPを呼出し(LWC処理)
  3. 【GCP】invoceiAgentのAPI「download_v3/raw」をコールしSFDCから指定したファイルをダウンロード
  4. 【GCP】ダウンロードファイルデータをレスポンスとして返す

LWC JSコード ダウンロード処理の抜粋

exeDownload(fid, fname) {
    getDownloadUrl({iaId: fid})
    .then(result => {
        this.friendlyMessage = undefined;
        this.error = undefined;
        if (result) {
            // ダウンロードURLをJS上から呼出しファイル出力とする
            let downloadElement = document.createElement('a');
            downloadElement.href = result;
            downloadElement.target = '_self';
            document.body.appendChild(downloadElement);
            downloadElement.click();
        }
    })
    .catch(error => {
        console.error('Error:getDownloadUrl->', error);
        this.friendlyMessage = fname + 'のダウンロードでエラー発生';
        this.error = error;
    });
}

カスタムオブジェクトを増やせないから紐づけはinvoiceAgentのカスタムプロパティを使用

Salesforceのレコードを表示した時に、そこからアップロードしたinvoiceAgentのファイルを表示する要件がありました。実現に際して、カスタムオブジェクトを増やしてはいけないというプロジェクトの制約がつきました。それは、『Lightning Platform Starter』ライセンスを使用しており、カスタムオブジェクト10個が上限で他の処理で使用するために、使用してはならないというものでした。
そこで、invoceiAgentのカスタムプロパティとして「オブジェクト」「オブジェクトID」という項目を持たせ、それぞれ、オブジェクトAPI名とオブジェクトID(18桁のID)を設定するようにしました。

仕組みとしては以下のように構築しました。

  1. 【SFDC】ファイルアーカイブのカスタムプロパティ値を組み立てる時に、「オブジェクト」「オブジェクトID」も入れる
  2. 【SFDC】invoceiAgentのAPI「search_v19/folder」をコール、カスタムプロパティのオブジェクトIDで検索するように指定

ファイルの情報はすべてinvoiceAgent上で持ち、Salesforceではその情報を元に表示しているだけという仕組みにしたのです。

Apexコード ファイル検索処理の抜粋

/** invoceiAgentファイル検索 */
    public List<IaObjectDocument> searchFile(String customeName, Id recordId) {
        List<IaObjectDocument> result = null;
        JSONGenerator gen = JSON.createGenerator(true);
        // カスタムプロパティの名前をIDに変換
        Map<String, IaObjectCustomProperty> customPropertiesName = getCustomPropertyList(true);
        String cusId = customPropertiesName.get(customeName)?.id;
        // 検索パラメータの作成
        // ルート配下のサブフォルダを含めて、文書のみを、カスタムプロパティの値で検索
        gen.writeStartObject();
        gen.writeFieldName('folderIds');
        gen.writeStartArray();
            gen.writeStartObject();
            gen.writeStringField('id', getIaRootFolderId());
            gen.writeEndObject();
        gen.writeEndArray();
        gen.writeStringField('operator', 'OR');
        gen.writeStringField('recursive', 'true');
        gen.writeFieldName('conditions');
        gen.writeStartArray();
        gen.writeStartObject();
        gen.writeStringField('conditionType', 'custom');
        gen.writeStringField('id', cusId);
        gen.writeStringField('type', 'equals');
        gen.writeStringField('value', String.valueOf(recordId));
        gen.writeEndObject();
        gen.writeEndArray();
        gen.writeEndObject();
        // 検索の実施(下のメソッド)
        result = search(gen);
        return result;
    }
    
    /** invoiceAgentの検索実行 */
    private List<IaObjectDocument> search(JSONGenerator gen){
        List<IaObjectDocument> result = null;
        HttpRequest req = new HttpRequest();
        Http http = new Http();
        // 認証情報設定
        Boolean authFlg = login(req);
        if (authFlg) {
            // HTTPコールアウト
            // https://cs.wingarc.com/manual/ia/doc/cloud/apiref/ja/3218132.html
            req.setEndpoint(getBaseUrl() + '/search_v19/folder');
            req.setMethod('POST');
            req.setHeader('Content-Type', 'application/json');
            req.setHeader('x-requested-with', 'XMLHttpRequest');
            req.setBody(gen.getAsString());
            HttpResponse res = http.send(req);
            debugResponse(res);
            // 結果の確認
            if (res.getStatusCode() == 200) {
                Map<String, Object> results = (Map<String, Object>) JSON.deserializeUntyped(res.getBody());
                List<Object> resultList = (List<Object>)results.get('resultList');
                if(resultList != null && resultList.size() > 0) {                    
                    result = new List<IaObjectDocument>();
                    for (Object obj:resultList) {
                        // 取得結果を変換
                        Map<String, Object> tmp = (Map<String, Object>)obj;
                        IaObjectDocument iaObj = setIaObjectDocumnt(tmp);
                        result.add(iaObj);
                    }
                } 
            } else {
                throw new InvoiceAgentException(res, InvoiceAgentException.CALLACTION.SEARCH);
            }
        }
        return result;
    }

使用するオブジェクトを後付けで設定できるようにカスタムメタデータ型の使用

今回のソリューションでは機能を使用するオブジェクトを設定により変更できる汎用的な仕組みが求められました。ApexやLWCのソース上には、オブジェクトの名前を直接記述しないやり方が必要であり、それを実現するために「カスタムメタデータ型」を使用することにしました。
「カスタムオブジェクト」「カスタム設定」「カスタム表示ラベル」と外から設定を与える方法は色々ありますが、複数行から構成される情報が必要であったこと、カスタムオブジェクトを増やせないことから、カスタムメタデータ型を使用することを選択しました。

カスタムメタデータ型として2つ定義を用意し、invoiceAgentを使用できるオブジェクト、保存フォルダ、カスタムプロパティの設定を管理しました。

  • 接続の情報を管理する『invoiceAgentSetting』
  • 文書の種類(帳票区分)毎にどのようなカスタムプロパティを設定するかを管理する
『invoiceAgent文書設定』
『invoiceAgent文書設定』の設定内容

カスタムメタデータでオブジェクトAPI名および使用する項目API名を設定します。
Apexクラスではメタデータを使用してレコードを取得します。

Apexコードの抜粋 カスタムメタデータを扱うクラスを用意し、その中で動的なSOQLを組み立てレコードを取得

/** 必要な項目をセットしたオブジェクトの取得 */
    public sObject getRecord(String recordId) {
        sObject sobj;
        String soqlStr = getSOQL();
        if (String.isNotEmpty(recordId) && String.isNotEmpty(soqlStr)) {
            soqlStr += ' WHERE Id=:recordId';
            // 組み立てたSOQLの実行
            sobj = Database.query(soqlStr);
        }
        return sobj;
    }
    /** 必要な項目をセットしたSOQLのSelect-From部分だけ */
    public String getSOQL() {
        String soqlStr;
        List<String> fields = new List<String>();
        // オブジェクトAPI名からスキマー情報を取得
        Map<String, Schema.SObjectField> sMap = Schema.getGlobalDescribe().get(this.objectApiName)?.getDescribe().fields.getMap();
        if (sMap != null) {
            fields.add('Id');
            // カスタムメタデータに設定されているフィールドだけをリストアップ
            for (invoiceAgentDoc__mdt data : this.metaDataList) {
                String fieldName = data.FieldAPIName__c;
                if (String.isNotEmpty(fieldName) && fieldName != '-' && sMap.get(fieldName) != null && !fields.contains(fieldName)) {
                    fields.add(fieldName);
                }
            }
        }
        // リストアップされたフィールドでSOQLを組み立てる
        if (fields.size() > 0) {
            soqlStr = 'SELECT ' + String.join(fields, ',') + ' FROM ' + this.objectApiName + ' ';
        }
        return soqlStr;
    }

所感

CRMデータに電帳法に対応された形で各種契約書ファイルが紐づいている状況は、顧客状況を把握する上で非常に分かりやすく、利便性が高いと感じました。通常のInvoiceAgentの運用では、InvoiceAgent側の機能やフォルダに対して顧客に紐づくファイルをアップロードしてSalesforce側で後ほど参照という形になります。
しかし、本ソリューションではSalesforceのUIから直感的にアップロード、参照が出来るため、利用者側の運用負荷が少なく、定着もすぐに実現することができたと感じています。

まとめ

システムの要件・制約の中で柔軟に対処を模索することが大切です。
1つのサービスだけで解決できない問題が発生した時には、別のサービスを組み合わてソリューションを構築することで目的を達することができます。


当社の構築したSalesforce×invoiceAgent×GCPのソリューション、またはサービスを組み合わせてのソリューション構築に興味がございましたら、お気軽にお問合せください。

参考情報

invoiceAgentのWeb APIを使ってみる
https://navi.wingarc.com/product/invoiceagent/11160

invoiceAgent Web APIリファレンス
https://cs.wingarc.com/manual/ia/cloud/ja/1567416.html