Salesforce Nonprofit Cloud向けLWC開発マスターガイド:開発者による徹底解説

背景と適用シナリオ

Salesforce Nonprofit Cloud (非営利団体向けクラウド) は、非営利団体 (NPO) が寄付者管理、資金調達、プログラム管理、マーケティング活動などを一元的に行うための強力なプラットフォームです。その中核をなす Nonprofit Success Pack (NPSP) は、柔軟なデータモデルと豊富な標準機能を提供し、多くの団体の業務を効率化しています。

しかし、団体の規模や活動内容が多様化するにつれて、標準機能だけでは満たせない独自の要件が出てくることがあります。例えば、以下のようなケースが考えられます。

  • 特定の支援者セグメントに特化した、インタラクティブな寄付履歴ダッシュボードを構築したい。
  • ボランティアが自身の活動時間や内容を簡単に入力できる、モバイルフレンドリーなカスタムフォームが必要。
  • 外部の決済システムと連携し、寄付情報をリアルタイムで登録する複雑なプロセスを自動化したい。

このような独自のユーザー体験 (UX) やビジネスロジックを実現するために、Salesforce 開発者の出番となります。本記事では、Salesforce のモダンな UI フレームワークである Lightning Web Components (LWC) (Lightning Webコンポーネント) を活用し、Nonprofit Cloud のデータモデルと連携してカスタムコンポーネントを開発する方法について、技術的な観点から深く掘り下げていきます。特に、支援者の寄付履歴を表示するカスタム LWC の構築を例に、具体的な実装方法を解説します。


原理説明

Nonprofit Cloud 向けの LWC 開発を成功させる鍵は、その根幹にある NPSP データモデルを正確に理解することです。標準の Sales Cloud とは異なる、非営利団体特有の概念がオブジェクトモデルに反映されています。

主要なオブジェクトとリレーションシップ

開発者が特に頻繁に扱うことになる、資金調達関連の主要オブジェクトは以下の通りです。

  • Account (取引先): NPSP では、「世帯 (Household Account)」と「組織 (Organization Account)」の2つの主要なレコードタイプが使用されます。個人支援者は、通常、世帯取引先に紐づく取引先責任者として管理されます。
  • Contact (取引先責任者): 支援者、ボランティア、理事など、団体に関わる個人を表します。必ず一つの世帯取引先に所属します。
  • Opportunity (商談): NPSP において「寄付」や「助成金」を管理する中心的なオブジェクトです。`Donation` (寄付)、`Grant` (助成金)、`Major Gift` (大口寄付) など、複数のレコードタイプが用意されています。開発時には、どのレコードタイプのデータを扱うかを意識することが重要です。
  • npe01__OppPayment__c (支払): 一度の寄付 (Opportunity) を分割して支払う場合(例:12万円の寄付を毎月1万円ずつ支払う)、その個々の支払いを記録するカスタムオブジェクトです。単発の寄付でも、通常はこの支払オブジェクトに1つのレコードが作成されます。
  • npsp__RecurringDonation__c (継続支援): 毎月や毎年など、定期的な寄付の契約情報を管理するカスタムオブジェクトです。このレコードが存在すると、設定に基づいて将来の寄付 (Opportunity) が自動的に作成されます。

これらのオブジェクトは相互に連携しています。例えば、ある Contact が寄付をすると、その寄付は Opportunity レコードとして作成されます。この Opportunity は Contact に関連付けられ、さらにその支払情報は npe01__OppPayment__c レコードに記録されます。このリレーションシップを理解せずに SOQL (Salesforce Object Query Language) を記述すると、期待通りのデータを取得できないため、開発に着手する前に必ずスキーマビルダーや関連ドキュメントでデータモデルを確認することが不可欠です。

サンプルコード:支援者の寄付履歴 LWC

ここでは、特定の支援者 (Contact) のレコードページに配置することを想定し、その支援者の過去の寄付履歴(商談)と生涯寄付合計額を表示する LWC を作成します。

1. Apex コントローラー: DonationHistoryController.cls

まず、LWC から呼び出すための Apex クラスを作成します。このクラスは、指定された Contact ID に基づいて関連する寄付情報をデータベースから取得する役割を担います。

// DonationHistoryController.cls
public with sharing class DonationHistoryController {

    /**
     * @description 指定された取引先責任者の生涯寄付合計額を取得します。
     *              NPSPが管理する標準の集計項目を利用します。
     * @param contactId 寄付履歴を取得する取引先責任者のID
     * @return 寄付合計額
     */
    @AuraEnabled(cacheable=true)
    public static Decimal getLifetimeGivingTotal(Id contactId) {
        // NPSPは取引先責任者オブジェクトに寄付の集計項目を自動で更新します。
        // Total_Number_of_Donations__c (寄付回数)
        // Total_Gifts_Amount__c (寄付合計額) など。
        // ここでは Total_Gifts_Amount__c を使用します。
        // クエリのパフォーマンスを向上させ、ガバナ制限を回避するために、直接集計項目を参照するのがベストプラクティスです。
        try {
            Contact c = [
                SELECT Id, npsp__Total_Gifts_Amount__c
                FROM Contact
                WHERE Id = :contactId
                WITH SECURITY_ENFORCED
                LIMIT 1
            ];
            // nullチェックを行い、値が存在しない場合は0を返します。
            return c.npsp__Total_Gifts_Amount__c != null ? c.npsp__Total_Gifts_Amount__c : 0;
        } catch (Exception e) {
            throw new AuraHandledException('Error retrieving lifetime giving total: ' + e.getMessage());
        }
    }

    /**
     * @description 指定された取引先責任者の最新の寄付(商談)を5件取得します。
     * @param contactId 寄付履歴を取得する取引先責任者のID
     * @return 商談のリスト
     */
    @AuraEnabled(cacheable=true)
    public static List<Opportunity> getRecentDonations(Id contactId) {
        // 商談オブジェクトから、指定された取引先責任者に関連するレコードを検索します。
        // NPSPでは、個人の寄付はContactのPrimary Contact Role (主取引先責任者の役割) を通じて商談に紐づきます。
        // OpportunityContactRole を介して検索するのが最も正確です。
        try {
            List<OpportunityContactRole> ocrList = [
                SELECT Opportunity.Id, Opportunity.Name, Opportunity.Amount, Opportunity.CloseDate, Opportunity.StageName
                FROM OpportunityContactRole
                WHERE ContactId = :contactId AND IsPrimary = TRUE
                ORDER BY Opportunity.CloseDate DESC
                LIMIT 5
            ];

            List<Opportunity> opps = new List<Opportunity>();
            for(OpportunityContactRole ocr : ocrList) {
                opps.add(ocr.Opportunity);
            }
            return opps;

        } catch (Exception e) {
            // エラーが発生した場合、AuraHandledExceptionをスローしてLWC側で検知できるようにします。
            throw new AuraHandledException('Error retrieving recent donations: ' + e.getMessage());
        }
    }
}

コード解説:

  • with sharing: このクラスが現在のユーザーの共有ルールに基づいて動作することを保証します。これにより、ユーザーがアクセス権を持たないデータが表示されるのを防ぎます。
  • @AuraEnabled(cacheable=true): LWC から安全に呼び出すことができ、かつクライアントサイドで結果をキャッシュできることを示します。これにより、サーバーへのラウンドトリップが減り、パフォーマンスが向上します。
  • getLifetimeGivingTotal: NPSP が自動で計算・保存している取引先責任者の集計項目 `npsp__Total_Gifts_Amount__c` を直接参照しています。SOQL で都度集計するよりもはるかに効率的です。
  • getRecentDonations: `OpportunityContactRole` オブジェクトを介して寄付を検索しています。これは、一個人が複数の寄付に関連する NPSP の標準的な方法です。`IsPrimary = TRUE` 条件で、その寄付の主たる支援者であることを確認しています。
  • WITH SECURITY_ENFORCED: 項目レベルおよびオブジェクトレベルのセキュリティがクエリに適用され、ユーザーが表示権限を持たない項目やオブジェクトにアクセスしようとするとエラーが発生します。
  • try-catch: エラーハンドリングを実装し、問題が発生した場合は `AuraHandledException` をスローして LWC 側で適切に処理できるようにしています。

2. LWC コンポーネント: donationHistory

次に、Apex のメソッドを呼び出し、取得したデータを表示する LWC を作成します。3つのファイル(HTML, JavaScript, XML)で構成されます。

donationHistory.html
<!-- donationHistory.html -->
<template>
    <lightning-card title="支援履歴" icon-name="standard:thanks">
        <div class="slds-p-horizontal_small">
            <!-- 生涯寄付合計額の表示 -->
            <template if:true={totalGivingAmount.data}>
                <p class="slds-text-heading_medium slds-m-bottom_medium">
                    生涯寄付合計額: 
                    <b>
                        <lightning-formatted-number
                            value={totalGivingAmount.data}
                            format-style="currency"
                            currency-code="JPY"
                        ></lightning-formatted-number>
                    </b>
                </p>
            </template>
            <template if:true={totalGivingAmount.error}>
                <p>合計額の読み込み中にエラーが発生しました。</p>
            </template>

            <hr/>

            <h4 class="slds-text-heading_small slds-m-bottom_small">最新の寄付履歴 (最大5件)</h4>
            <!-- 寄付履歴の表示 -->
            <template if:true={recentDonations.data}>
                <template if:false={hasDonations}>
                    <p>寄付履歴はありません。</p>
                </template>
                <template if:true={hasDonations}>
                    <table class="slds-table slds-table_cell-buffer slds-table_bordered">
                        <thead>
                            <tr class="slds-line-height_reset">
                                <th scope="col"><div class="slds-truncate" title="寄付名">寄付名</div></th>
                                <th scope="col"><div class="slds-truncate" title="金額">金額</div></th>
                                <th scope="col"><div class="slds-truncate" title="完了日">完了日</div></th>
                                <th scope="col"><div class="slds-truncate" title="ステージ">ステージ</div></th>
                            </tr>
                        </thead>
                        <tbody>
                            <template for:each={recentDonations.data} for:item="donation">
                                <tr key={donation.Id}>
                                    <td data-label="寄付名"><div class="slds-truncate">{donation.Name}</div></td>
                                    <td data-label="金額">
                                        <lightning-formatted-number
                                            value={donation.Amount}
                                            format-style="currency"
                                            currency-code="JPY"
                                        ></lightning-formatted-number>
                                    </td>
                                    <td data-label="完了日">
                                        <lightning-formatted-date-time value={donation.CloseDate}></lightning-formatted-date-time>
                                    </td>
                                    <td data-label="ステージ"><div class="slds-truncate">{donation.StageName}</div></td>
                                </tr>
                            </template>
                        </tbody>
                    </table>
                </template>
            </template>
            <template if:true={recentDonations.error}>
                <p>寄付履歴の読み込み中にエラーが発生しました。</p>
            </template>
        </div>
    </lightning-card>
</template>
donationHistory.js
// donationHistory.js
import { LightningElement, api, wire } from 'lwc';
import getLifetimeGivingTotal from '@salesforce/apex/DonationHistoryController.getLifetimeGivingTotal';
import getRecentDonations from '@salesforce/apex/DonationHistoryController.getRecentDonations';

export default class DonationHistory extends LightningElement {
    // レコードページのコンテキストから現在のレコードIDを自動的に受け取ります。
    @api recordId;

    // @wire を使用して Apex メソッドを呼び出し、結果をリアクティブにプロパティにバインドします。
    // recordId が変更されると、このメソッドは自動的に再実行されます。
    @wire(getLifetimeGivingTotal, { contactId: '$recordId' })
    totalGivingAmount;

    @wire(getRecentDonations, { contactId: '$recordId' })
    recentDonations;

    /**
     * @description 取得した寄付データが空でないかどうかを判定するゲッター。
     *              HTMLテンプレート内で条件分岐に使用します。
     * @returns {boolean} 寄付データが存在する場合は true
     */
    get hasDonations() {
        return this.recentDonations.data && this.recentDonations.data.length > 0;
    }
}
donationHistory.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>58.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__RecordPage">
            <objects>
                <object>Contact</object>
            </objects>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

コード解説:

  • @api recordId: この LWC がレコードページに配置された際に、Salesforce から自動的にそのページのレコード ID (この場合は Contact ID) を受け取るための公開プロパティです。
  • @wire: Apex メソッドをリアクティブに呼び出すためのデコレーターです。`{ contactId: '$recordId' }` のように、`$` を接頭辞として付けることで、`recordId` プロパティの変更を監視し、値が変わると自動的に Apex メソッドを再実行します。結果は `totalGivingAmount` と `recentDonations` プロパティに格納され、`data` と `error` の2つの属性を持ちます。
  • hasDonations (ゲッター): JavaScript のゲッター構文を使い、`recentDonations.data` が存在し、かつ空の配列でないかを判定します。これにより、テンプレート側で `if:true={hasDonations}` のような直感的な記述が可能になります。
  • .js-meta.xml: この LWC がどこで利用できるかを定義する設定ファイルです。`isExposed` を `true` にし、`target` を `lightning__RecordPage` に設定することで、Lightning アプリケーションビルダーで利用可能になります。`objects` タグで、このコンポーネントを配置できるオブジェクトを `Contact` に限定しています。

注意事項

Nonprofit Cloud 環境でカスタム開発を行う際には、以下の点に特に注意が必要です。

権限 (Permissions)

LWC や Apex は実行ユーザーの権限コンテキストで動作します。コンポーネントが正しく機能するためには、ユーザープロファイルまたは権限セットで、関連オブジェクト (Contact, Opportunity, OpportunityContactRole など) への参照アクセス権と、表示する項目への項目レベルセキュリティ (Field-Level Security) が適切に設定されている必要があります。`WITH SECURITY_ENFORCED` を SOQL に含めることで、これを強制できます。

API 制限とガバナ制限 (API Limits and Governor Limits)

Salesforce プラットフォームには、すべての組織がリソースを公平に利用できるように、ガバナ制限が設けられています。特に、一度に大量のデータを扱うバッチ処理や、多数の支援者情報を一括で更新するような機能を実装する際は、SOQL クエリの発行回数 (100回/トランザクション)、DML ステートメントの実行回数 (150回/トランザクション)、CPU 時間などを意識した設計が不可欠です。Apex コードは常にスケーラビリティを念頭に置いて記述する必要があります。

エラー処理 (Error Handling)

Apex 側では `try-catch` ブロックを使用して例外を捕捉し、LWC には `AuraHandledException` をスローしてエラー情報を渡すことが重要です。LWC 側では、`@wire` の結果オブジェクトの `error` プロパティをチェックし、エラーが発生した場合にはユーザーに分かりやすいメッセージを表示するべきです。これにより、予期せぬ問題が発生した際にも、ユーザーが混乱しない堅牢なアプリケーションを構築できます。

NPSP の自動化との共存

NPSP には、寄付 (Opportunity) が「商談成立」になると自動的に支払 (Payment) レコードを作成したり、住所情報を更新したりするなど、多くのバックグラウンド自動化処理が組み込まれています。カスタムコードで DML 操作を行う際は、これらの自動化プロセスを意図せず妨害したり、二重に実行したりしないよう注意が必要です。例えば、寄付レコードを Apex で作成する際は、NPSP が提供する TDTM (Table-Driven Trigger Management) フレームワークの動作を理解し、必要に応じてトリガーを一時的に無効化するなどの高度な制御が求められる場合があります。


まとめとベストプラクティス

本記事では、Salesforce 開発者の視点から、Nonprofit Cloud のデータモデルを活用してカスタム LWC を構築する方法を解説しました。NPSP の標準的なオブジェクト構造を理解し、Apex と LWC を組み合わせることで、非営利団体の固有のニーズに応える、柔軟で高性能なソリューションを開発できます。

ベストプラクティス

  1. データモデルを深く理解する: 開発に着手する前に、必ず NPSP の公式ドキュメントやスキーマビルダーでオブジェクト間のリレーションシップを確認してください。特に、`Account` の世帯モデルや `OpportunityContactRole` の役割は重要です。
  2. NPSP の集計項目を活用する: 生涯寄付額や今年の寄付額など、NPSP が自動で計算してくれる集計項目が多数存在します。パフォーマンス向上のため、可能な限りこれらの項目を直接参照し、リアルタイムでの複雑な集計クエリは避けてください。
  3. テストクラスを徹底する: 非営利団体にとって寄付データは最も重要な資産の一つです。Apex コードには、必ず網羅的なテストクラスを作成し、単体テスト、正常系・異常系のシナリオ、バルク処理をテストして、コードの品質とデータの整合性を保証してください。
  4. ハードコーディングを避ける: `Donation` のようなレコードタイプ ID やデベロッパー名をコード内に直接書き込むのではなく、カスタムメタデータやカスタム表示ラベルから取得するように設計することで、将来のメンテナンス性が大幅に向上します。
  5. 標準機能を尊重する: 新しい機能を開発する前に、まず Flow や標準コンポーネントで実現できないかを検討してください。コードは最後の手段と捉え、Salesforce の「クリックで設定、コードで拡張」の哲学に従うことが、長期的な運用コストを抑える鍵となります。

Salesforce Nonprofit Cloud は強力なプラットフォームですが、その真価は、開発者がその上に付加価値の高いカスタムソリューションを構築することで最大限に引き出されます。この記事が、皆さんの開発プロジェクトの一助となれば幸いです。

コメント