Jewelry AI、購入はどう解決されるか
ストアからの通知は検索キーであって、何を受け取るかを決める権限ではありません
購入が完了したとき、ストアが送るメッセージが、それが何になるかを決めているわけではありません。メッセージは識別子を運び、その識別子はサービス側の商品レコードで検索されます。購入がどのアプリに属し、どんな付与を生むのかを持っているのは、そのレコードです。検索が有効なレコード1件に定まらない場合、付与は近似されるのではなく止められます。
関わるレコードは2つ、決めるのはその片方だけです
購入が完了すると、ストアから通知が届きます。その通知は、確実に運んでいると信頼できるものだけが読まれます。ストアの商品識別子と、イベントを説明するポリシーのフィールドです。その識別子を使ってサービス側のデータベースから商品レコードを探し、購入がどのアプリに属し、どんな付与を生むのかを言うのは、そのレコードです(apps/api/src/webhooks/revenuecat.service.ts、ソースツリーを2026-08-18に確認)。
これをはっきり書く価値があるのは、直感的な理解が逆だからです。購入はストアで起きたので、ストアのメッセージのほうが権限を持つように感じられます。コードの中では、それは入力であり、権限はサービス側で保守しているレコードの1行にあります。
この仕組みから導かれない結論が1つあり、それは引き出すべきではありません。ストアや送信側が正しい金額を送っていることは、ここでは何も示されていません。この設計は、その問いに答えるのではなく、その問いに依存しないようにしています。
通知が持ち込むもの、商品レコードが決めるもの
この分担が仕組みのすべてです。このページのそれ以外の内容は、どの問いがどちらの側で答えられるかから導かれます。
| 問い | 答えるもの |
|---|---|
| どのストア商品についての話か | 通知が運ぶ識別子。検索キーとして使われます |
| 購入はどのアプリに属するか | サービス側のデータベースにある商品レコード |
| 購入はどんな付与を生むか | サービス側のデータベースにある商品レコード |
| その付与はどれだけの大きさか | 商品レコード。そのために通知が読まれることはありません |
| 購入はどの環境から来たか | 通知。解決の一部として読まれます |
推測せずに付与を止める条件
いずれも、解決できない検索に対して処理を拒むことです。ここではそれぞれが何をするかで説明します。このページは、それらが実行される順序を主張しません。
- 識別子が複数のアプリに解決される。
- イベントは一意に定まらないものとして扱われ、内部確認のために保留されます。同じ検索が同じあいまいさを生むため、再試行には意味がありません(apps/api/src/webhooks/revenuecat.service.ts)。
- アプリ間で識別子が一意であることは、運用上の規律です。
- これはコードが強制する保証ではなく、カタログ管理上の落とし穴です。検査は衝突を見つけますが、衝突が記録されること自体は防ぎません。
- 商品レコードが無効になっている。
- 提供を終了した商品の購入はクレジットを生まず、代わりに内部確認のために保留されます。
- 購入がストアのテスト環境から来た。
- 本番の構成では付与の経路は既定で閉じていて、明示的な宣言があったときにだけ開きます。閉じている間、イベントは無期限に再試行されるのではなく、無視というはっきりした結果に達します。本番でその設定がどうなっているかは、コードからは読めません。
提供を終了した商品が今も見つかる理由と、それが意図的である理由
商品の提供終了は、レコードを無効にするだけで取り除きはしません。したがって検索では今も見つかり、拒否はもっとあとの、クレジットを付与する経路で起こります(apps/api/src/webhooks/revenuecat.service.ts)。
ここから、矛盾と取り違えられやすい非対称が生まれます。提供を終了した商品の購入は何も付与せず内部確認のために保留される一方、同じ商品の返金や期限切れのイベントは通常どおり処理されます。理由は単純です。提供終了がすべての経路を塞いでしまうと、すでに付与されたクレジットをあとから取り戻せなくなります。
環境の関門も同じ形をしています。閉じるのは付与の経路だけです。返金の側では環境が検索キーの一部になるため、テスト環境で起こした返金は、そもそも処理すべき本物の購入を見つけません。
それぞれの状況が何に落ち着くか
真ん中の列は、読み手が取れる行動ではなく、システムが行うこととして読んでください。内部確認は内部の結果です。
| 状況 | 選択 | 理由 |
|---|---|---|
| 識別子が、1つのアプリの有効な商品1件に解決される。 | そのレコードから付与が作られる。 | 購入がどのアプリのどんな付与に対応するかについては、そのレコードが権限を持ちます。 |
| 識別子が複数のアプリに定義されている。 | イベントは止められ、内部確認のために保留される。 | 検索が一意に定まらず、レコード自体にあるあいまいさは再試行では解決できません。 |
| 識別子が提供を終了した商品に解決され、イベントが購入である。 | クレジットは生まれず、イベントは内部確認のために保留される。 | 提供終了はレコードを無効にし、関門は付与の経路にあります。 |
| 識別子が提供を終了した商品に解決され、イベントが返金または期限切れである。 | 通常どおり処理される。 | その経路を塞ぐと、すでに付与されたクレジットを取り戻せなくなります。 |
| 本番の構成で、購入がストアのテスト環境から来た。 | 本物のクレジットは書き込まれず、イベントは無視として終了する。 | 付与の経路は明示的な宣言で開かれない限り閉じていて、無期限に再試行するよりも終端の結果のほうが望ましいからです。 |
このページが扱わないこと
- 金額、価格、パックやプランの名称、通貨。ここで支えられる記述は、付与の大きさが商品レコードから来るということだけです。
- 上で説明した検査が適用される順序。それぞれは何をするかで説明されており、並びの中での位置では説明されていません。
- 同じ通知が複数回届いたとき、あるいは通知が順序どおりに届かなかったときに何が起こるか。厳密に1回だけという動作は、このページでは主張しません。
- 本番でのテスト環境の設定の値。既定はコードから読み取りましたが、配備されている構成は読み取っていません。
- 設定フラグ、環境変数、検証のためのパラメーターの名称。このページの元になった読み取りの、意図的な範囲外です。
- ストアや送信側が何を送るかをどう決めているか、どんな間隔で再送するか。コードはインターフェースを前提としており、相手側の振る舞いを証明するものではありません。
- クレジットが生まれたあとどう使われるか、残高がどう計算されるか。それはシステムの別の部分に属します。
- ここで説明した仕組みが、現在配備されている版であるかどうか。読み取ったのは、このワークツリーにおける2026-08-18時点のソースツリーです。
よくある質問
購入がどれだけのクレジットを生むかは、ストアのメッセージが決めているのですか?
決めていません。通知はストアの商品識別子と、それが運ぶポリシーのフィールドのために読まれ、その識別子がサービス側の商品レコードで検索されます。そのレコードが、商品の属するアプリと、生まれる付与を持っています(apps/api/src/webhooks/revenuecat.service.ts、ソースツリーを2026-08-18に確認)。この仕組みは、送信側が正しい金額を報告していることを示すものではありません。その問いが生じないように作られています。
同じストアの商品識別子が複数のアプリに登録されていると、どうなりますか?
検索が複数の一致を返し、そのイベントは一意に定まらないものとして扱われ、再試行されずに内部確認のために保留されます。再試行しても同じあいまいさに行き当たるからです。アプリ間で識別子を一意に保つことはカタログ上の運用の規律であり、コードが強制するものではありません。検査は衝突を見つけるものであって、防ぐものではありません。
提供を終了した商品の購入が何も生まないのに、その返金は通るのはなぜですか?
商品の提供終了はレコードを削除せずに無効にするため、検索では今も見つかり、関門はクレジットを付与する経路だけにあります。購入はクレジットを生まずに内部確認のために保留され、同じ商品の返金と期限切れのイベントは通常どおり処理されます(apps/api/src/webhooks/revenuecat.service.ts)。この非対称は意図的です。すべての経路を塞ぐと、すでに付与されたクレジットを取り戻せなくなるからです。
ストアのテスト環境で行った購入は、本物のクレジットを生みますか?
本番の構成では、既定では生みません。テスト環境の購入に対して付与の経路は閉じていて、明示的な宣言があったときにだけ開きます。閉じている間、イベントは無期限に再試行されるのではなく、無視というはっきりした結果に達します。関門が掛かるのは付与だけです。返金の側では環境が検索キーの一部になるため、テスト環境の返金は処理すべき本物の購入を見つけません。本番でその設定がどうなっているかはコードからは読めず、ここでは述べません。