ContractS開発者ブログ

契約マネジメントシステム「ContractS CLM」の開発者ブログです。株式会社HolmesはContractS株式会社に社名変更しました。

複数フィールドのバリデーションエラーを集約して表示するVeeValidate v4の活用法

こんにちは。ContractSでフロントエンドエンジニアをしている北原です。

弊社では、UIのバリデーションとしてVeeValidate v4を採用しています。 この記事では、複数のインプットを1つのバリデーションフィールドとして扱う方法について解決策を提示したいと思います。

目次

前提知識

今回の記事の前提知識です。VeeValidate v4についてご存知の方はスルーしてください。

VeeValidateとは?

Vue.jsにおけるフォームバリデーションを簡素化する人気のライブラリです。
日本語ドキュメントも充実しており、大変扱いやすいものになっています。 vee-validate.logaretm.com

VeeValidate v4 の Composition関数

1. useField

const { value, errorMessage } = useField(() => 
  'email',
  yup.string().email().required(),
)

useFieldは、コンポーネントをフィールドとして扱うことができる関数です。

v-modelにbindして使う実際の入力値valueや、バリデーションルールにそぐわない場合に生成されるerrorMeesageなどを有しています。 vee-validate.logaretm.com

2. useForm

const { handleSubmit } = useForm()

useFormは、記述したコンポーネントをフォームとして扱うことができる関数です。

前述したuseFieldを有するコンポーネントを子コンポーネント以下に持つ場合に用いることで、その値を取得して送信できるhandleSubmitなどを有しています。 vee-validate.logaretm.com

なぜ必要なのか?

弊社のプロダクトでは、契約書を管理するに当たって契約項目と呼ばれる関連情報を、各契約書が有しています。
それらをユーザの方へ入力していただく場合に必須項目としたいパターンが存在します。

このとき、以下のような別々のフィールドを、エラーメッセージを1箇所に集約してバリデーションをしたいパターンが出てきました。

エラーメッセージを1箇所に集約したい例

このフィールドは、契約書に対する自動更新機能を有効にしたとき、契約書の契約期間終了後に新しく引き延ばす契約期間の長さを入力するフィールドです。
左側は値(半角数字)のインプット、右側は単位(日、ヶ月、年から選択)のプルダウンとなっており、2つで1つです。

しかしVeeValidateの仕様上、このようなパターンでは左右それぞれのフィールドが、個々のバリデーションを持つ別のフィールド値として扱われてしまうため、上の画像のようにエラーメッセージの表示箇所を集約できません。

なんとか解決する手段を考えた結果がこの記事になります。

TL;DR

  1. エラーメッセージを自身の管理対象として加える関数をprovideするラッパーコンポーネントを作成
  2. useFieldを持つコンポーネントが、1. のコンポーネントからprovideされた関数をinjectしてエラーメッセージを管理対象に加える

以上の流れで実現します。

やってみる

参考

VeeValidate v4開発者、Abdelrahman AwadさんのCodeSandboxにあった以下の内容を参考にしました。

https://codesandbox.io/p/sandbox/handling-nested-forms-in-vee-validate-v4-p45up?file=%2Fsrc%2Fcomponents%2FChildForm.vue%3A30%2C3-53%2C3

実装例

1. ラッパーコンポーネントの作成

まず以下のように、動的にuseFieldの値を複数管理するための関数を提供するラッパーコンポーネントを実装します。

script

// Types
interface Injection {
  errorMessage: Ref<string>
  // 他に管理したい対象があれば追加する
}
export type ProvideAddField = (injection: Injection) => void

// Variables
const errorMessages = ref<Ref<string>[]>([])

// Computed
const allErrorMessages = computed<string[]>(() => (errorMessages.value.map(it => it.value).filter(it => !!it)))

/**
 * エラーメッセージを登録する関数をprovideする
 */
onBeforeMount(() => {
  provide(ProvideKey.INCLUDE_FIELD_ERROR_MESSAGE,
    (injection: Injection) => {
      errorMessages.value.push(injection.errorMessage)
    },
  )
})

onBeforeMountのライフサイクルで、このコンポーネントへエラーメッセージを登録する関数をprovideしておき、それによって追加されるエラーメッセージ群のerrorMessagesも持っておきます。

また、実際にエラーが発生しているものだけに絞り込んだallErrorMessagesも合わせて持っておきます。

template

<template>
  <div>
    <slot />
    <!-- 複数のメッセージを表示したい場合はv-forすれば良い -->
    <AtomsErrorMessage v-show="allErrorMessages.length !== 0" class="mt-1" :message="allErrorMessages[0]" />
  </div>
</template>

template部には、provideした関数により追加されたエラーメッセージの表示箇所と、当該コンポーネントへエラーメッセージを追加したいフィールドコンポーネントを入れ込むためのslotを記述します。

2. フィールドコンポーネントから関数をinject

script

const { value, errorMessage } = useField(() => 
  'email',
  yup.string().email().required(),
)

// (中略)

/**
 * エラーメッセージを、ラッパーコンポーネントの管理対象として追加します ※
 */
const includeField = (errorMessage: Ref<string>) => {
  const injection: ProvideAddField | undefined = inject(ProvideKey.INCLUDE_FIELD_ERROR_MESSAGE)
  if (injection) injection({ errorMessage })
}

onMounted(() => {
  includeField('email', errorMessage)
})

フィールドコンポーネントへ、onBeforeMountedでprovideされている関数を、次のonMountedライフサイクルで呼出す処理を追加しておきます。

※ ここでは便宜上素で定義していますが、composablesにしておくのが良いです

3. 実際に使ってみる

ラッパーコンポーネントのslotへ、実際にバリデーションしたいフィールドコンポーネントを入れこみます。

これにより、エラーが発生された場合に1箇所へ集約して表示できるようになります。

テンプレート記述例

まとめ

実装自体に手間はかかりますが、一度仕組み化してしまえば汎用的に利用できるため利便性は高いと思います。
もう少し簡素に実現できる方法がありましたら、ぜひご教授ください。

今回の記事が、少しでも同じような課題を抱えている方の助けになれば幸いです。

最後に

お読みいただきありがとうございました。
ContractSでは一緒にプロダクトを進化させていくエンジニアを募集中です。

recruit.jobcan.jp

recruit.jobcan.jp

recruit.jobcan.jp

APIファーストで契約管理システムの連携力を高める

こんにちは。ContractSでバックエンドエンジニアをしている毛見です。
弊社がAPIファースト開発を取り入れた背景や利点、実際のアプローチを整理してブログにしました。

契約ライフサイクル管理 (CLM: Contract Lifecycle Management) は、単なる電子契約、契約書の保管という枠を超え、営業、法務、財務、購買などの多様な業務との連携によって業務の統制や効率化を図ります。昨今の企業では、業務ごとに専用のSaaSや基幹システムを導入しており、これらのシステムとシームレスに連携することがCLMに求められています。
私たちは、この連携を柔軟に実現するために APIファーストのアプローチを採用しています。この記事では、その利点や実践内容、得られる効果についてご紹介します。

APIファーストとは?

APIファーストとは、システムやサービスの設計・開発において 「まずAPIを定義することから始めるアプローチ」 です。このアプローチでは、APIを単なる技術的なインターフェースではなく、システム全体の基本構造として捉えます。
参考: Postman What is API-first?

APIファーストの利点

利用者視点のインターフェース設計

APIの定義を先に行うことで、フロントエンド、他システム、外部の開発者の視点を意識した設計が可能になります。それにより他システムやパートナーとの連携が迅速で容易になります。

ノーコード/ローコードでシステム構築ができる世界を実現する

APIの定義において、RESTやOpenAPI(Swagger)などの業界標準を採用することで、ノーコード/ローコードでの連携が可能になり、外部システムやSaaSとの連携がスムーズになります。

柔軟なユースケース対応

ビジネス要件に応じてAPIを再利用・組み合わせることで、多様なユースケースに柔軟に対応できます。新しい製品やサービスを迅速に立ち上げる際にも、APIファーストの設計は大きな強みとなります。

APIファーストをどのように取り入れているか?

設計フェーズと実装フェーズ

APIの仕様書を作る方法として、実装後にコントローラーから生成する方法があります。それに対し、APIファーストの開発では、まず設計フェーズでAPI仕様をOpenAPIで記載します。記載したAPI仕様について開発者、プロダクトオーナーで合意し、テストケースを作成します。この時点で、外部サービスとの連携、既存のWebサービスで利用できるAPIとなるように検討を済ませます。その後に実装フェーズに移ります。

既存のWebアプリケーション用のバックエンドからストラングラーフィグパターンで移行する

ContractS ではAPIファーストを取り入れる以前のWebアプリケーション用のバックエンドサービスが存在します。そのバックエンドサービスには仕様が蓄積されていますが、外部サービスとの連携を考慮していないため、APIとして公開できません。そのため、公開も可能なAPIサービスとして作り直す必要があります。そこで、機能ごとにストラングラーフィグパターンで新たなAPIに移行をしました。既存のバックエンドサービスは移行先のAPIを呼ぶようにし、BFF(backend for frontend)として振る舞うことで、フロントエンドへの影響を最小限にとどめながら移行が可能になります。

テストと品質保証の自動化

実現の途中ではありますが、CI/CDによってAPIのE2Eテストを自動化し、相互運用性の担保を行います。これによりWebアプリケーションだけでなく、連携先のAPIに対しても品質を保証することができます。

ビジネス的な効果

顧客への導入提案において、Webアプリケーションだけでは実現が難しい要求に対し、API連携を利用した実現方法を提案できるため、「APIを使ってできます」と言えるようになります。 また、他システムとの連携について詰めていく中で、想定外の要件がでることもあるかと思います。そういった時に、特定のユースケースのためのAPIではなく、汎用的なAPIとして設計してあれば、それを組み合わせるだけで追加開発の必要なく要件に対応することができます。

最後に

契約ライフサイクルは、さまざまなシステムとの連携なしには成り立ちません。そのため、私たちは APIを核に柔軟な連携基盤を構築しています。これにより、顧客の多様な業務要件に応え、契約管理の効率化を推進しています。
実現途中のAPIのE2Eテストについてもブログ化を検討していますので、楽しみにしていただければと思います。

ContractSでは一緒にプロダクトを進化させていくエンジニアを募集中です。

recruit.jobcan.jp

recruit.jobcan.jp

recruit.jobcan.jp

Pub-Subモデルを活用して通知基盤を改善した話

というタイトルで、12/13(金)に開催されたDisruptors Tech Meetに登壇しました。
こんにちは。ContractSの友野です。

Disruptors Tech Meetとは

株式会社ディスラプターズのグループ会社に所属するエンジニアが

  • 各社事例を見て・聞いて刺激をもらう
  • 普段触れない技術の話を聞いてスキルアップに繋げる
  • 発表や意見交換の経験を積む

といったことを目的に、日頃の成果を発表・共有する勉強会です。
ContractSはディスラプターズ(キャリアインデックスから商号変更)グループの一員として、継続的なアウトプットの場として活用しています。

同会には、キャリアインデックス社、マージナル社のエンジニアが参加しており、毎度ワイワイと楽しんでいます。

Disruptors Tech Meetは、グループ会社間のクローズドな勉強会ではあるものの、クオーター毎に継続開催され、そろそろ3年目に突入します。
いずれパブリックな勉強会にしていく狙いもあります。お楽しみに。
(今回は登壇側でしたが、筆者は普段は運営側の人間です)

Springにおけるアプリケーションイベント

登壇資料では具体的な技術スタックに触れておりませんが、Java/Spring Boot環境で基盤は構築しています。
Springでは、アプリケーションイベントの発行はApplicationEventPublisherで行います。

ApplicationEventPublisher (Spring Framework API) - Javadoc

余談ですが、数年前まで(Spring 4.2以前)はイベント発行のためにApplicationEvent(Javadoc)というクラスを継承する必要がありましたが、今現在はPOJOのデータモデルが使えるので、今回の通知基盤だったり、ドメインイベントへの適用だったり、活用はかなりしやすくなったと感じています。

発行されたイベントは、TransactionalEventListenerアノテーションを付与したメソッドでリッスンします。評価されるタイミングはデフォルトはトランザクションコミット後(AFTER_COMMIT)です。

TransactionalEventListener (Spring Framework API) - Javadoc

サンプル

以下、簡単なコードイメージです(イメージしやすさ優先で、適切な責務分割ではないです…)。

// イベント発行側
public class ApplicationEventNotifier {
    // 通知用文面ファクトリ
    private final NotifiableContentsFactory notifiableContentsFactory;
    private final ApplicationEventPublisher applicationEventPublisher;

    // タスクが作成されたことをユーザーに通知する
    public void taskCreated(Set<UserId> destinations) {
        destinations.stream()
                .map(userRepository::findById)
                .map(notifiableContentsFactory::taskCreated)
                .forEach(this::notify);
    }

    private void notify(Notifiable event) {
        applicationEventPublisher.publishEvent(event);
    }
}


// タスク作成イベント
public record TaskCreated(MailAddress destination, String subject, String body) implements Notifiable {
}


// イベント購読側
public class NotifiableEventListener {
    private final MailClient mailClient;

    // タスク作成時に呼ばれるリスナー
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onTaskCreated(TaskCreated event) {
        mailClient.send(event.destination(), event.subject(), event.body());
    }
}

このサンプルだけ見ると効果は感じにくいかもしれませんが、実際には通知イベントインタフェースの統合やリスナーの共通化によって、フレームワークのような形で使えるようにしています。
今回はアプリケーションの構造を整理したのみで、メッセージブローカーの導入などまでは踏み込んでいません。いずれこのあたりも拡張後、記事にまとめていこうと思います。

最後に

簡単ではありますが、登壇資料の公開と、Springにおけるアプリケーションイベントの実装サンプルをまとめました。今回の文脈では通知のみですが、また別の形でドメインイベントへ適用も進めています。こちらも機会を見つけてまとめておこうと思います。

最後に、ContractSでは一緒にプロダクトを進化させていくエンジニアを募集中です。少しでも興味がある方、「CLM?なにそれ美味しいの?」という方もカジュアル面談から可能ですので、以下のページからお気軽にお声掛けください。

recruit.jobcan.jp

recruit.jobcan.jp

recruit.jobcan.jp

Vue 3, Nuxt 3 マイグレーションを経験してみて

はじめに

こんにちは。 ContractSでフロントエンドエンジニアをしている村澤です。

先日、一時的に Vue 2.x から Vue 3.x のマイグレーションへ携わる機会があったため、その間の学びを共有します。

ContractS CLMでは、Vue.js を用いて開発をしています。

Vue.jsとNuxt.jsは、Web開発において広く利用されている人気の高いフレームワークです。
技術の進化とともに、それらのバージョンも進化を続けています。Webフレームワークのマイグレーションは、開発者にとって重要なタスクです。本記事では、その必要性と経験について述べます。

Vue 3, Nuxt 3へのマイグレーションがなぜ必要なのか

Vue 2は、2023年にEOL(End of Life)を迎えました。

v2.vuejs.org

これは、Vue 2の新しい機能やセキュリティパッチが提供されなくなったことを意味します。また、Nuxt 2.xの依存関係にあたるライブラリが更新されることも無くなるため、脆弱性が発見された際のパッチ適用が難しくなります。
したがって、品質の維持とセキュリティの確保のために、Vue 3およびNuxt 3へのマイグレーションが必要です。

破壊的変更の学習

Vue 3への移行には、破壊的変更を伴います。これは、Vue 2から廃止された機能、仕様の大きな変更などが含まれます。 その中でも影響が大きいと感じた変更はv-modelの扱いです。
Vue 3 からは以下のような仕様変更がなされています。

バージョン デフォルトプロパティ名 デフォルトイベント名
Vue 2 value input
Vue 3 modelValue update: modelValue

Vue 2では、propsの.sync修飾子で、子コンポーネントからのemit イベント名をupdate:(prop名)とする仕様がありました。 対してv-modelのデフォルトイベント名は、上記の表のように.syncと形式が異なったため、Vue 3ではv-modelにおいても同様の仕様に統一された形になります。 このような変更について学習し、変更する必要があります。

v3-migration.vuejs.org

プラグイン・ライブラリの選定

Vue 2で使用していたプラグインやライブラリの多くは、Vue 3に対応していないもの、もしくはVue 3に対応するために仕様変更されたものがあります。これにより、移行コストが高くなります。さらに、Vue 3に対応する代替ライブラリが存在しない場合もあります。
このような場合、開発者は自らライブラリを更新するか、あるいは代替手段を模索する必要があります。また、既存のライブラリとの互換性の問題も発生する可能性があります。

例えば、Nuxt 3.8で[email protected]を導入すると、ビルドエラーが発生するという問題があります。このような問題に対処するためには、開発者は適切な代替手段を見つける必要があります。

Vue 2コンポーネントの移行コスト

今まで Vue 2で記述されていたコンポーネントを、すべて Vue 3 の記法へ変更する必要があります。 プロダクトの大きさにもよりますが、CCLMの場合300近くのコンポーネントがあり、これをすべて書き換えるとなると膨大な時間がかかることは想像に容易いと思います。
しかし、この効率をあげる方法に画期的なものはなく、現在有力なのはVue 2のプロジェクトをVue 2.7までバージョンを上げ、バックポートされているVue 3の機能を取り入れることです。

blog.vuejs.org

マイグレーション全体を通して

上記に書ききれなかった学びを簡単に記述します。

  • 公式推奨のプラグイン・ライブラリでもアプリケーションに合わないことがある。

    • ex) ストーリー記述としてStoryBookを推奨しているが、Viteでのエイリアス解決失敗とビルドエラーにより、Historeを採用
  • 思い切って既存のプラグイン・ライブラリを別のプラグイン・ライブラリに変更する
    • ex) BootstrapVueからPrimeVueに変更
  • 原因不明のビルドエラーが発生し、解消できない場合はプラグイン・ライブラリのissuesを確認する

さいごに

他にも多くの課題がありますが、今回は主要な課題を取り上げました。 マイグレーションは現在も進行中ですが、上記に上げたように多くの学びが有り、貴重な経験でした。

【Vue.js】Painless なコンポーネント開発のプラクティス

はじめに

こんにちは!ContractS株式会社の北原です。
フロントエンドのリードとして、プロジェクトの技術選定や品質保守等を担当しています。

早速ですが...

みなさまは Vue 3 をお使いになられていますか?
これには、以前の Vue 2 と比較して多種多様な改善がふんだんに盛り込まれています。
2020/09/18 にリリースされてから暫く経っているので、実際の業務や個人で触られた方も多いと思います。

例えば、

  • Composition API の正式採用
  • script setup 構文の対応
  • メモリ使用量削減による負荷軽減

その他色々ありますが、以上のような機能により、従来のメジャーバージョンからご利用になっていた開発者からすると嬉しいものが満載です。

しかし、ただひとつ。 頭を悩ませる要素があるのです。
それこそが本稿を作ったきっかけです。

:deep 機能について

Vue 3 には:deepという擬似クラスとして利用できる機能があります。
これは、親コンポーネントから、子コンポーネントの特定要素に対してスタイルを当てたい時に避けては通れない道です。

:deep の具体的な使い方については、以下の公式ドキュメントをご覧ください
ja.vuejs.org

:deep 機能のつらいところ

Atomic Design程やらないまでも、多くのデザインシステムではatomsのように基底コンポーネントを作成し、各コンポーネントで使う形だと思います。

自社で全ての基底コンポーネントをスクラッチで作成し利用すれば、SFC で運用しても自由にスタイルをカスタマイズできるので問題ないですが、多くのプロジェクトでは Vuetify や Quasar のようなUIライブラリをご利用になると思います。

また、そのままの見た目で利用することは少なく、UX統一等の理由でスタイルを適用すると思います。
当然ライブラリなので、日々バージョンアップが行なわれていきます。その際にコンポーネントのDOM構造が変化することもあるでしょう。

:deepは前述の通り擬似クラスであり、CSSセレクタである以上、UIライブラリのコンポーネントのDOMを知らないといけません。
方法としては様々あると思いますが、検証ツール等を用いてスタイルを当てたい要素を探し、その要素を一意に特定できるセレクタを作ってスタイルを当てる必要があります。

これを頻繁にメンテナンスするのは高コストで、コンポーネントに新規のバリエーションを持たせたい時にもスピーディな開発を阻害します。

そんなあなたに PrimeVue

PrimeVue は、Vue 3 で利用可能なUIライブラリのひとつであり、90をも超える多彩なコンポーネントと堅牢なFWを提供しています。
今回紹介する機能を抜きにしても、単品として高いアクセシビリティを誇るので、是非以下で触ってみてください。

primevue.org

PrimeVue の Pass Through 機能

Pass Through (パススルー) 機能は、ドキュメントで以下のように説明されています。

The Pass Through props is an API to access the internal DOM Structure of the components.

要するに、このAPIを利用することでPrimeVueのコンポーネント内部のDOM構造へアクセスが可能になるというわけです。
すでに:deepで感じていた pain を解決できる匂いがしますね。

Pass Through の利用方法

PrimeVueで提供される全てのコンポーネントには、ptという名称のpropsが用意されています。
そこに、PrimeVueのDOM毎の命名をkeyとして持つObjectをbindすることで、DOMのカスタマイズが可能です。

これだけだと分かりづらいと思うので、今回は Vue Button Component を具体例として挙げます。

ご覧いただくと、PASS THROUGHタブ内で、画像のようにDOM毎に連番が割振られていると思います。

PrimeVue Button の Pass Through 連番

ここで、4番のラベル部分のスタイルをカスタマイズしたいとします。
そのまま下にスクロールしていただくと、対象の連番に対して命名があると思います。

PrimeVue Button の Pass Through 命名

labelという命名がなされていますね。
これをkeyとした Object を、SFC上で以下のようにbindします。

PrimeVue Button の Pass Through Bind

Tailwind CSS のクラスであるfont-boldを適用してみました。

なんとこれだけで、PrimeVue Button のDOM構造を知らずともスタイルが適用できてしまいます。
画像のようにcomputedを bind することで動的なスタイル変更も可能です。
今回はTailwind CSSを利用しましたが、グローバルなUtility Classとも非常に相性がいいです。

最後に

いかがでしたでしょうか?

:deepで大変な目に遭われた、もしくは今まさに遭われている方もいらっしゃるかと思います。私も同じで、Nuxt 2からNuxt 3へマイグレーションを行なう際にとても痛い目を見ています...笑

もし良さそう!と感じていただけましたら、導入は以下で簡単に行なえますので、是非お試しください!

出典

グループ会社間で行なった勉強会の内容となります。
本稿よりも、もう少し実際の利用例や他機能も交えた解説を行なっていますので、よろしければご覧ください!

非エンジニアがMarketingAutomationを使わずに顧客向けメール配信を完全自動化した話

こんにちは。そしてはじめまして。

ContractS コーポレートサクセス部のぽよさんこと新井です。

僕は職業エンジニアではないのですが、職務の一つとして社内の業務システムの管理者を受け持っており、仕事柄多くのツール(特にSaaS製品)を駆使して業務設計する機会が多く、「あ、組み合わせるとこんな感じの動きができるのね」というネタが出てきます。 そんなネタを紹介してみようと思います。

今回出てくるツール

  • Salesforce・・・CRM/顧客管理ツール。以下SF。

  • Pardot・・・MarketingAutomation(MA)ツール。セールスフォース社のMAでSFと連携/連動している。(※現:Account Engagemant)

  • GoogleAppsScript・・・GWSにも含まれる、Googleのプログラミングツール。以下GAS。

事の発端

当社はContractS CLMというSaaSサービスを提供していまして、お客様のサービス利用契約はサブスクリプションモデル/期間契約となっています。

当社のカスタマーサクセスチームより、「お客様に『あなたの会社の契約更新日はmm月dd日ですよ』のようなメール通知を特定のタイミングでお送りしたい。」とのリクエストをもらいまして、早速話を聞いてみました。

会話する中で決まった要件が下記なのですが、、、

1. 一斉配信メールではあるが、顧客の利用代表者様に適切にメールを届けたい。営業目的ではないため、MAで有している宛先ごとのOptOut設定を問わず送信したい。
 (※OptOut設定・・・メールアドレスの持ち主が、MA等を用いて送信されるメルマガ/営業系メールなどを送られるのを止める設定のこと。メール配信停止、などと表記することが多い。 なおこのケースでは、お客様の契約に係る重要な要件となるため、営業系メールと異なりOptOutに関わらずメールは送信したい。)

2. メール配信タイミングは毎月月初で定期実行。月1回のみでよい。

3. 配信元データはすべてSFの中にある情報を用いて条件設定を行う。元データの所在がわかりやすく、かつ条件式は設計者以外も認識しやすいレベルに噛み砕いたものを希望したい。

4. 対象顧客の抽出条件は契約更新日を用いる。例えば1月月初のメールでは、翌翌々月中(=4月中)が契約更新日となっているお客様を宛先としたい。

5. 業務負荷やヒューマンエラーを回避するために理想は完全な自動化。難しいとしても、手動での毎回の抽出条件変更、宛先リストアップ、メールへの記入は極力避けたい。最初に設定した条件を毎月変更せずに使い続けたい。

 (※ちなみにContractS CLMのサービス利用契約は比較的柔軟に組み合わせられるようにした都合、サービスプロダクト側にユーザの利用契約に関する情報は入れない仕様になっています。)

これらを受け、検討を始めます。

出てきた壁、課題

PardotでOptOut設定の如何に関わらずメール送信すること自体は可能なのですが、上記4の抽出条件を満たす方法があるか、という点が出発点となりました。

SF(Pardot)のレポート抽出は比較的柔軟性が高いので、「相対日付」の条件を用いることを検討したのですが、「翌翌々月」という相対日付がSFでは設定できません。

次に、「翌翌々月」を「(本日から見て)90日後~120日後」と読み替えて相対日付とする案を考えました。ただこれはメール送信する月日によっては月初日と月末日をピッタリ捉えることができないことに気が付き、断念しました、、、。

結局、SF&Pardotで対象顧客を抽出を完結することは難しそうだな、という途中経過に至ります。

ここでこの課題をクリアすべく編み出した方法が、今回ご紹介する内容です。

この方法であれば、一部GASでのコーディング(メール一斉配信部分)は必要となりますが、比較的難易度が低く汎用性が高いものなので、

  • 元データは当初の希望通りSFレコードを用い、レポートでの抽出条件は相対日付で設定。日付が変わったりSFレコードが更新されれば(多少のラグはあるが)自動的に反映される。

  • 対象の抜き出し、リスト整形はGoogleSpreadsheetで柔軟に実行 & 数式を組めばもちろん自動反映。

という形で元データの収集を自動化することができ、かつGASで仕込んだメール一斉配信ツールを定期発火できれば、自動化は万事OK。という算段です。

以降で順を追ってご紹介します!

Step1:Salesforceでレポートを準備

まず、『例えば1月には、翌翌々月(=4月)に契約更新日(今の契約期間の最終日)がくるお客様を宛先としたい。』という難儀な条件を含むSFレポートを作成します。 前述の通り、日付ドンピシャの検索条件は作れないため、下記条件の組み合わせで少し広めに設定します。

① 一致しない = 翌70日間

AND

② 一致する = 翌130日間

日付条件の組み合わせ

この組み合わせで、「常に今日より71日先から130日先までの日付を持ったレコードを抽出する」という条件が組めました。

相対日付を2つ組み合わせることで、データの絞り込みを効かせる方法です。
実際には、1月頭であれば、ざっくり3月半ば〜5月上旬の日付を抽出する形ですね。

Step2:Salesforce to SpreadSheetで自動レポート

次に、SFのデータをSpreadsheetにサクッと展開する方法を設定します。

SFのデータを出力、といえば王道は「レポートのエクスポート(.csv/.xlsx)」もしくは「データローダ(.csv)」かと思いますが、今回はSpreadsheetでSFのデータを扱いたいので、
Salesforce Connector を利用します。
↓この機能です

support.google.com

この機能を有効にしておけば、GoogleSpreadsheetにSalesforceからバシャっとデータをエクスポートすることができます。 詳細/機能全体の説明は割愛しますが、今回は、

  • 「Report」でSpreadsheetに取り込む

  • 「Auto Refresh」を設定して定期更新する

の2段構えです。

まずReportで、Step1にて作成したレポートを出力してみます。

Report

※出力イメージ

Spreadsheetでの出力イメージ

SFレポートの内容が、そのままシートに貼り付けられます。

次に、RefreshからAuto Refreshを選択します。

Auto Refreshは、一度Reportをしていると、そのReportを再度Refreshすることができる機能です。

Refresh

これによって、最短4時間ごと更新ですが、「常に最新のSFレポートをSpreadsheetに出力する」を実現しています。

ちなみにですが、この工程はレポートが2つ以上あっても有効です。
(それぞれ別のシートタブにレポートを出力→Auto Refresh設定1つで全レポートを定期更新できる)

検証中は、Auto Refreshは設定せず、一度Reportを実行したあとに見出し以外のデータレコードをダミーに書き替えることをおすすめします。

Step3:Spreadsheetでのデータ整形(QUERY関数)

元データを抽出できるようになったら、データを加工するところはGoogle先生に託します。
Spreadsheetでデータの抽出といえばQUERY関数が優秀です。

=QUERY('★元データのシート名★'!A:C,"select* where B>date'"&TEXT(eomonth(today(),+2),"YYYY-MM-DD")&"' and B<=date'"&TEXT(eomonth(today(),+3),"YYYY-MM-DD")&"'")

という形で、常に『翌翌々月の1ヶ月間に契約更新日が来る』レコードだけを更に抽出するようにします。
Step2でQUERY抽出したシートを、送信リストとして用います。

Step4:GASでメール一斉配信

続いてメール一斉送信です。
参考までに、私が利用しているスクリプトをサンプルで貼っておきます。
動作はSpreadsheet上ですので、コンテナバインドのGASエディターを使うのが良いかと思います。

//メールを一括送信するコード
function sendMail(){
  const spreadsheet = SpreadsheetApp.getActive();
  
  //送信先リストに使うシートをアクティブにしてデータを取得しにいく
  spreadsheet.setActiveSheet(spreadsheet.getSheetByName("★送信先リスト★")); 
  const sheet = SpreadsheetApp.getActiveSheet();
  
  //2行目から最終行までループ処理を行う
  const lastRow = sheet.getLastRow();
  for(let i = 2; i <= lastRow; i++){
 
    //行ごとに1列目を取得
    let account_name = sheet.getRange(i, 1).getValue();
    //行ごとに2列目を取得
    let renewal_date = sheet.getRange(i, 2).getValue();
     renewal_date = Utilities.formatDate(renewal_date, "JST", "yyyy/MM/dd");
    //行ごとに3列目を取得
    let to = sheet.getRange(i, 3).getValue(); 

  //メールのテンプレートがあるシートをアクティブにして内容を取得
  spreadsheet.setActiveSheet(spreadsheet.getSheetByName("★メールテンプレート★")); 
  const templateSheet = SpreadsheetApp.getActiveSheet();

    //B1セルはメールの件名として取得
    const subject = templateSheet.getRange(1, 2).getValue();

    //行ごとにB2セルのメール本文を取得して文章内の{取引先名}をそれぞれ上で定義した変数で置換
    const message = templateSheet.getRange(5, 2).getValue()
    .replace('{取引先名}',account_name)
    .replace('{更新予定日}',renewal_date)

    //メールの送信元を指定
    let options = {
    from: "",
    name: "",
    cc: ""
  };
  options.from = templateSheet.getRange(2, 2).getValue();
  options.name = templateSheet.getRange(3, 2).getValue();
  options.cc = templateSheet.getRange(4, 2).getValue();

    //取得した内容をGmailで送信
    GmailApp.sendEmail(to,subject,message,options);
  }
}

メールテンプレートのシートはこのような形ですね。

メールテンプレートのシートイメージ

ここまで完了すると、「(SFから自動転記をした上で)対象者を更に絞り、Spreadsheetの内容をもとにメールを一斉配信する」が行えるようになります。

Step5:GASのトリガー設定

最後にGASのトリガー設定を行います。

私はトリガー設定を、下記のように設定しています。
このあたりは、他のメール一斉配信のタイミングなどを鑑みて適宜調整いただくのがいいですね。

GASトリガー

終わりに

本件は、メール一斉配信のGAS以外はノーコードで、比較的簡素に仕上がっています。

GASのコードに関しても、汎用性高めなのと、パラメータ部分を変更するだけでおおよそ変化にも追従できる仕様になっていると思います。
こなれてくると、SF元データが正しい前提にはなりますが、他のメール配信系のアクションでも完全自動化させることが可能です。(弊社も順次、完全自動を増やしています。)

GASでのメール一斉配信は、裏側でGAS設定者のGmailアカウントから送信する仕様ですが、
メールが増えてくると、必然的にMail Send Failureとして返ってくるメールも設定者のGmailに返ってくる数が増えてきます。

「自動でメール送ったはいいけどFailureで返ってくる数多いなあ。1件ずつメーラーでアドレス確認して確認依頼を社内に回すの面倒だなぁ」と怠惰な気持ちが芽生えてくるのですが、長くなってしまうのでこのあたりはまた別の機会に、、、。

また、SFのAutoRefreshも、実はこのままだとFail発生時に気付くことができないので、こちらもまた別の機会に、、、。

お読みいただきありがとうございました。もう1本、他のメンバーの記事を見ていただけるととても嬉しいです!

イベントストーミング体験ワークショップに参加した学びと感想

こんにちは。テックリードの友野です。最近、急に寒くなったもので、衣替えが追いついていません。

さて、11/10(金)にUMTP主催のModeling Forum 2023ワークショップ「ドメインモデリングの強力なツール: Event Stormingを体験しよう」に参加してきたので感想と、メモの整理を兼ねてポストします。
ドメインイベントの伝播による整合性担保やその仕組みはContractS CLMで部分的に採用していますが、その領域はごくわずかです。イベントストーミングという言葉そのものは知っていましたが、業務では採用しておらず、経験もありません。今回のワークショップを通じて、多くの学びがありました。

umtp-japan.org

(2023/11/15 追記:UMTP事務局よりmiroキャプチャ利用許可をいただいたので、画像を追加しました)

ワークショップ概要

ワークショップは、イベントストーミングを体験し、慣れることを目的としています。
1チーム4~6人で構成し、イベントストーミングのプロセスに沿って議論しながらmiro上でモデリングをしました。テーマは図書館業務。誰もが知っている/利用したことがある一方で、その業務についてすべてを理解している人はいないことが理由だそうです。図書館司書と利用者、2つのアクターそれぞれの業務を参加者全員(30人!)で分担して整理しました。
具体的には、書籍"Learning Domain Driven Design”(以下、LDDD)で紹介されているステップに沿って(一部省略して)進めていく形式です。ステップバイステップで進めていく中で生まれた疑問点や不明点を都度、講師(Chatwork加藤さん/GMOインターネット成瀬さん)に聞けるという贅沢な時間でした。

図書館業務のユースケースサンプル
図書館業務のユースケースサンプル

イベントストーミングとは

イベントストーミングはブレインストーミングのドメインイベント版です。つまり、イベントをとにかく発散させて徐々に収束させていくドメインモデリングの手法です。このアプローチは、ドメイン上の重要な関心ごとであるイベント=出来事(コト)に着目しています。
イベントに着目する理由として、以下があります。

  • イベントが分かれば、そのイベントを生み出したふるまいが分かる
  • イベントはヒトやモノと必ず関係があり、コンテキストがあるので収束させやすい
    • 逆にモノからアプローチすると、複数コンテキストがある場合があり、発散させやすい

イベントストーミングはビッグピクチャー、プロセスモデリング、ソフトウェアデザインから構成されます*1。参加者の知見や認識を揃えながら、業務の流れを整理したいシーンにはフィットしそうです。

イベントストーミングで使う付箋と依存の方向
イベントストーミングで使う付箋と依存の方向

進め方

1. イベントを整理する(ビッグピクチャー)

ワークショップでは、まず以下の流れでイベントを整理しました。

  1. イベントを書き出す
    • イベントは出来事なので動詞の過去分詞形で表現し、この段階では他者との重複は気にしない。
      • 例:利用者を登録した、蔵書を貸し出した
  2. イベントを精査する
    • 同じもの・似ているものをまとめる
      • 特に、用語のゆらぎに注意。ユビキタス言語となりうる。
    • イベントはドメインの状態変化を表すため、確認や閲覧のような状態を変化させないものは除外する
  3. イベントを左から右へ時系列に並べる
    • 同時に起きるイベントは縦に並べる
  4. 時系列を順に読み合わせて矛盾がないか確認する(ウォークスルー)
    • 例えば、登録していない情報を突然更新していないか など
    • 足りなければ、この段階でイベントを追加する

※ LDDDに記載されているフェーズについては、ワークショップではスキップしました。

イベントストーミングでは議論が白熱したり、横道に外れたりした場合は、ホットスポットとしてメモを残して議論を先へ進めます。例えば、「発注した本の支払い方法は銀行振り込みかクレジットカード払いか」という論点は図書館業務の整理から見れば、ホットスポットです。

2. イベントからプロセスの流れを見つける(プロセスモデリング)

コマンドとアクター/ポリシーをつなげる

イベントが揃ったら次は、そのイベントを生み出すふるまいの整理です。

  1. イベントからコマンドを作る
    • コマンドはリクエストなので動詞の現在形(命令形)として表現する。日本語の場合、体言止めは分かりにくいので非推奨とのこと。
    • 蔵書を貸し出した(イベント)→蔵書を貸し出す(コマンド)
  2. コマンドをリクエストするアクターを配置する
  3. システムが自動的/連鎖的にリクエストする場合はポリシー
    • ポリシーは次のコマンドをリクエストするトリガーのようなもの
    • ポリシーの例:本を入荷した(イベント)->管理番号採番ルール(ポリシー)->管理番号を採番する(コマンド)

コマンドを実行するためのリードモデルを定義する

コマンドを実行するために何かしら情報が必要であり、この情報こそがリードモデルです。システム外のリードモデルも表現しておくのが肝です。

  1. リードモデルを追加する
    • 複数のリードモデルからコマンドを実行する場合もある
      • 例えば、蔵書を貸し出す(コマンド)ために、蔵書(リードモデル)と貸出状況(リードモデル)が必要
  2. どのイベントからリードモデルが作られるのかを紐づける
    • 蔵書を貸し出した(イベント)後に、貸出状況(リードモデル)が更新される
  3. システム外のリードモデルも表現しておく
    • 利用者登録時の身分証明書 など

外部システムを追加する

モデリング対象の業務とは直接関係のない外部システムがある場合は、議論の発散を避けるため明示するだけに留めます。

3. コマンドとイベントから集約を見つける(ソフトウェアデザイン)

最後に一連の流れを俯瞰してみて、コマンドを受け取り、処理結果としてイベントを生み出す集約を見つけます。
当たり前ですが、このステップは機械的には出来ず、コマンドとイベントからどのような概念があるのか議論が必要です。イベントストーミングを用いない、ヒアリング中心のドメインモデリングでも変わらず難しいステップです。集約は名詞で表現するのが一般的なので、その命名に相応しい責務なのか、分割/統合するならライフサイクルは適切かという観点が重要です。

感想

終日議論をし続けて、疲労感と充実感で満たされたワークショップでした。イベントストーミング初体験でしたが、良い点と注意点が見えてきました。

イベントストーミングの結果
モデリングの様子
チームで発見した集約
チームで発見した集約

良い点

議論を進めやすい

モデリングするフレームワークなので当たり前と言えば当たり前ですが、ステップバイステップで参加者の意識・興味を集中できるので、議論が進めやすく感じました。ワークショップの同じチームメンバーとは初対面、かつ図書館業務経験者(ドメインエキスパート)不在でしたが、適度な発散と収束を繰り返しながら、メンバー全員が納得いく集約の定義まで時間内に進めることができました。

集約検討時に前提の認識を揃えやすい

一つ目と似たような観点ではありますが、イベントに着目した業務の流れを参加者全員で作り上げていくため、いざ集約の議論を始めようとするとそこまでの知識が揃った状態で始められます。場合により、発散させたいケースもあるかもしれませんが、こと問題領域を明らかにしたいケースにおいて、参加者の認識が揃った状態で議論が開始できるのはとても強力です。

要求の曖昧さを排除できる

イベントが時系列に並び、ウォークスルーで整合性を担保できるので、ユースケースの確からしさを検証できます。ContractSでは予備設計としてロバストネス分析を一部領域に採用していますが、近しいものを感じました。繰り返し実施することで曖昧さを排除し、不確実性を減らしながら価値検証するアプローチにフィットしそうです。

注意点

効果を最大化するためにはアーキテクチャの制約がある

注意が必要なのは、イベントストーミングの効果を最大化するためにはCQRS(Command-Query Responsibility Segregation)+イベントソーシングの構成が前提となっている点です。
ふるまいの結果として生み出されたドメインイベントが記録され、そのイベントからリードモデルが作られる前提のため、CQRSのようにモデルレベルで分離した構成が必要ですし、何よりイベントを記録していくためにイベントソーシングを採用していないと、イベントをイベントのまま永続化できません。
ワークショップでは成瀬さんがステートソーシングによる実装方式をライブコーディングしてくれましたが、コードからイベントの知識は消えているので、いずれモデルとコードに乖離が発生してアジリティを失う懸念があります。

逆に言えば、CQRS+イベントソーシングの構成であれば、これほど強力なモデリングフレームワークはないとも思います。

終わりに

初体験のイベントストーミングでしたが、とても興味深いものでした。適用するにはアーキテクチャ観点で若干のハードルがあるものの、エンジニア職以外のメンバーと共通言語で会話する最初の一歩として効果がありそうです。幸い、JavaにはAxon Frameworkというイベントソーシングをサポートするフレームワークがあるため、小さく検証することもできそうです。試した結果はまた別記事でまとめようと思います。

*1:必ずしもこれらのプロセスをしなければならない、というわけではないようです。