1. はじめに
SaaSビジネスにおいて、サブスクリプション管理基盤の移行は「心臓移植」にも例えられるほどリスクの高い手術です。今回、私たちはHubSpot Commerce Hubで管理していた数百件のサブスクリプションを、Stripe Billingへ移行するプロジェクトを完遂しました。
なぜ移行するのか?
HubSpot Commerce Hubは、CRMと統合されている点で営業チームにとっては非常に使いやすいツールです。しかし、ビジネスが成長し、経理・財務的な要件が厳格になるにつれて、以下の課題が浮き彫りになってきました。
- 入金消し込み(Reconciliation)の辛さ : 銀行振込やクレジットカード決済の消し込み作業が手動に近く、経理チームの負担が限界に達していた。
- 集計と分析の限界 : MRR(月次経常収益)やChurn Rate(解約率)の正確な算出において、HubSpotのデータ構造では柔軟なクエリが難しかった。
Stripe Billingへの移行は、これらの課題を一掃し、より堅牢な決済基盤を構築するための必然的な選択でした。
プロジェクトのゴール
目標はシンプルかつ困難なものでした。
- ダウンタイムゼロ : 既存顧客の課金サイクルを止めることなく移行する。
- データの完全性 : 契約期間、金額、割引条件を1円のズレもなく移行する。
2. 初期設計の失敗:Clean Architectureの「罠」
プロジェクト開始当初、私たちは「教科書通り」の設計を行おうとしました。Go言語を採用し、Clean Architectureを意識した構成です。
汎用エンティティの幻想
「HubSpotにもStripeにも依存しない、純粋な『サブスクリプション』を定義しよう」
そう考えた私たちは、ドメイン層に汎用的な Subscription エンティティを定義し、HubSpotからの変換層と、Stripeへの変換層を分けようとしました。
しかし、このアプローチはすぐに破綻しました。
疑似コードで見る複雑性
HubSpotとStripeでは、言葉の定義が微妙に、しかし決定的に異なります。
// 失敗した設計:汎用エンティティを作ろうとした
type GenericSubscription struct {
ID string
Items []GenericItem
StartDate time.Time
// HubSpotの "Deal" なのか "LineItem" なのか?
// Stripeの "Subscription" なのか "Invoice" なのか?
// 両方の概念を無理やり統合しようとして、フィールドが肥大化
HubSpotSpecificField string // 結局こういうフィールドが必要になる
StripeSpecificField string
}
func ConvertHubSpotToGeneric(h hubSpotData) GenericSubscription {
// ここで大量の条件分岐が発生
// HubSpotの割引ロジックとStripeのクーポンロジックの整合性が取れない
} 「抽象化」を目指したはずが、結果として生まれたのは「両方のシステムの複雑さを足し合わせた、誰にも理解できない巨大な構造体」でした。これは典型的な “Wrong Abstraction”(誤った抽象化) の罠でした。
3. アーキテクチャの再構築:実用的な疎結合
私たちは方針を転換しました。「汎用的なサブスクリプション」など存在しないことを認め、 HubSpotのデータをStripeのデータへ「翻訳」すること に特化したアーキテクチャへ移行しました。
Direct Conversion (直接変換)
ドメイン層には「移行ロジック」そのものを配置し、データ構造の抽象化は諦めました。
コードの比較
新しいアプローチでは、マッピングが明確になりました。
// 成功した設計:目的特化型の変換
func (s *MigrationService) BuildStripeSubscriptionParams(
hsDeal *hubspot.Deal,
hsLineItems []hubspot.LineItem,
) (*stripe.SubscriptionParams, error) {
params := &stripe.SubscriptionParams{
Customer: stripe.String(targetStripeCustomerID),
// ...
}
for _, item := range hsLineItems {
// HubSpotのLineItemを直接StripeのPrice/Quantityにマッピング
priceID, err := s.findMatchingStripePrice(item)
// ...
}
return params, nil
} この変更により、コード量は半分以下になり、バグの発生源も特定しやすくなりました。
4. ワークフローの進化:「一発実行」から「CSV駆動」へ
アーキテクチャが決まった後、次に直面したのは「データ品質」の問題です。
HubSpotの自由入力という壁
HubSpot Commerce Hubは柔軟性が高く、営業担当者が手動で価格を変更したり、独自の割引を適用したりすることが可能です。 APIから取得したデータを見て、私たちは愕然としました。
- 正規価格と異なる金額が入力されている
- 「商品名2個」という商品が2個分の価格で登録されている
- 商品名が手動で書き換えられている
これをプログラムだけで自動判定してStripeに移行するのは、あまりに危険でした。「意図」を汲み取る必要があったのです。
Report -> Review -> Create ワークフロー
そこで私たちは、完全自動化を諦め、 「人間によるレビュー」をプロセスに組み込んだCSV駆動のワークフロー を構築しました。
CSVフォーマットの設計
CSVは、移行ツールが「判断に迷う部分」を人間が埋められるように設計しました。
| hubspot_deal_id | hubspot_product_name | hubspot_amount | stripe_price_id | stripe_coupon_id | stripe_tax_rate_id | note |
|---|---|---|---|---|---|---|
| 12345 | Basic Plan (Discounted) | 9000 | price_Hoge... | coupon_Special10 | txr_JP_10 | 手動で10%オフされていたためクーポン適用 |
| 67890 | Enterprise Plan | 50000 | price_Fuga... | (空欄) | txr_JP_10 |
ツールはHubSpotのデータを元にCSVの左側(現状)を埋め、右側(移行先)を推測して出力します。人間はそれを見て、推測が間違っている箇所や、空欄になっている箇所(自動判定不能なもの)を修正します。
この「CSV駆動」のアプローチにより、エンジニアはロジックの実装に集中し、契約の整合性確認はビジネスサイドに委譲することができました。
5. 技術的なこだわり
CSV駆動とはいえ、ツール側でも多くの工夫を凝らしました。
Price Matching (既存価格の再利用)
Stripeでは、同じ金額・通貨・期間(月次/年次)であれば、既存の Price オブジェクトを再利用すべきです。無駄に Price を量産すると、後の分析が困難になるからです。
ツールは以下のようなロジックで既存のPriceを検索し、CSVのデフォルト値として提案するようにしました。
// StripeのPriceリストをキャッシュしておき、金額と通貨でマッチング
func FindPrice(amount int64, currency string, interval string) *stripe.Price {
// ... lookup logic
} Tax Rates (税率の自動適用)
日本のインボイス制度対応のため、Stripe側で設定されたデフォルトの税率(消費税10%)を自動的に取得し、サブスクリプションに適用しました。これにより、移行後の請求書も法的に正しい形式で発行されます。
Metadataによるトレーサビリティ
移行後のトラブルシューティングに備え、Stripeの Customer および Subscription オブジェクトの metadata に、HubSpot側のIDを必ず付与しました。
-
metadata["hubspot_deal_id"]: “1234567890” -
metadata["migration_source"]: “hubspot-commerce-hub”
これにより、Stripeのダッシュボードを見ただけで「これはどのHubSpot案件から来たものか」が即座に分かります。
6. まとめ
HubSpotからStripeへの移行プロジェクトを通じて、私たちは以下の教訓を得ました。
- 過剰な抽象化を避ける : 1回限りの移行スクリプトに、Clean Architectureの厳格な適用はオーバーエンジニアリングになりがちです。
- データは汚いと仮定する : SaaSの自由入力フィールドは、エンジニアの想定を超えた使われ方をしています。
- Human-in-the-loop : すべてを自動化しようとせず、重要な意思決定(価格の確定など)を人間が行えるインターフェース(今回はCSV)を用意することが、結果としてプロジェクトを加速させます。
技術的な美しさよりも、 「確実に、間違った請求を送らない」 というビジネス上のゴールを最優先にした結果、泥臭くも堅実な「CSV駆動」という解に辿り着きました。この知見が、同様の移行プロジェクトに挑む誰かの助けになれば幸いです。