TECH BLOG

HubSpot Commerce HubからStripe Billingへのサブスクリプション移行

subscriptions
subscriptions

1. はじめに

SaaSビジネスにおいて、サブスクリプション管理基盤の移行は「心臓移植」にも例えられるほどリスクの高い手術です。今回、私たちはHubSpot Commerce Hubで管理していた数百件のサブスクリプションを、Stripe Billingへ移行するプロジェクトを完遂しました。

なぜ移行するのか?

HubSpot Commerce Hubは、CRMと統合されている点で営業チームにとっては非常に使いやすいツールです。しかし、ビジネスが成長し、経理・財務的な要件が厳格になるにつれて、以下の課題が浮き彫りになってきました。

  1. 入金消し込み(Reconciliation)の辛さ : 銀行振込やクレジットカード決済の消し込み作業が手動に近く、経理チームの負担が限界に達していた。
  2. 集計と分析の限界 : MRR(月次経常収益)やChurn Rate(解約率)の正確な算出において、HubSpotのデータ構造では柔軟なクエリが難しかった。

Stripe Billingへの移行は、これらの課題を一掃し、より堅牢な決済基盤を構築するための必然的な選択でした。

プロジェクトのゴール

目標はシンプルかつ困難なものでした。

  • ダウンタイムゼロ : 既存顧客の課金サイクルを止めることなく移行する。
  • データの完全性 : 契約期間、金額、割引条件を1円のズレもなく移行する。

2. 初期設計の失敗:Clean Architectureの「罠」

プロジェクト開始当初、私たちは「教科書通り」の設計を行おうとしました。Go言語を採用し、Clean Architectureを意識した構成です。

汎用エンティティの幻想

「HubSpotにもStripeにも依存しない、純粋な『サブスクリプション』を定義しよう」 そう考えた私たちは、ドメイン層に汎用的な Subscription エンティティを定義し、HubSpotからの変換層と、Stripeへの変換層を分けようとしました。

diagram
diagram

しかし、このアプローチはすぐに破綻しました。

疑似コードで見る複雑性

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”(誤った抽象化) の罠でした。

entity
entity

3. アーキテクチャの再構築:実用的な疎結合

私たちは方針を転換しました。「汎用的なサブスクリプション」など存在しないことを認め、 HubSpotのデータをStripeのデータへ「翻訳」すること に特化したアーキテクチャへ移行しました。

Direct Conversion (直接変換)

ドメイン層には「移行ロジック」そのものを配置し、データ構造の抽象化は諦めました。

diagram
diagram

コードの比較

新しいアプローチでは、マッピングが明確になりました。

// 成功した設計:目的特化型の変換
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駆動のワークフロー を構築しました。

diagram
diagram

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案件から来たものか」が即座に分かります。

dashboard
dashboard

6. まとめ

HubSpotからStripeへの移行プロジェクトを通じて、私たちは以下の教訓を得ました。

  1. 過剰な抽象化を避ける : 1回限りの移行スクリプトに、Clean Architectureの厳格な適用はオーバーエンジニアリングになりがちです。
  2. データは汚いと仮定する : SaaSの自由入力フィールドは、エンジニアの想定を超えた使われ方をしています。
  3. Human-in-the-loop : すべてを自動化しようとせず、重要な意思決定(価格の確定など)を人間が行えるインターフェース(今回はCSV)を用意することが、結果としてプロジェクトを加速させます。

技術的な美しさよりも、 「確実に、間違った請求を送らない」 というビジネス上のゴールを最優先にした結果、泥臭くも堅実な「CSV駆動」という解に辿り着きました。この知見が、同様の移行プロジェクトに挑む誰かの助けになれば幸いです。

グリーグループのグリーエックス株式会社で、ソフトウェア開発に従事。広告システム開発、GREE Platformの立ち上げ、不正利用対策、チャットアプリ開発、メディア開発を経て2025年2月より現職。好きなサウナ施設は、鶯谷のサウナセンター。