Stripe を包む層の境界を、stripe-customer spec・design.md・openspec/project.md が別々に決めている
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Idoneità per principianti
- 25/100
- Tipo di issue
- Documentazione
- Chiarezza
- Da chiarire
- Stato di attività
- Attiva
- Stack tecnologico
- typescript
- Ambito
- backend-api-design, documentation
Direzione di ricerca
Start by reading openspec/specs/stripe-customer/spec.md, design.md, openspec/project.md, and the comments in packages/api/src/index.ts and packages/api/src/payments/normalize.ts at the referenced commits. Review related issues #53, #15, and #87 before deciding which boundary and record are authoritative; done means the decision is documented consistently and the affected code comments or specifications no longer contradict it.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
症状
Stripe を包む層の境界について、3 つの記録が別々のことを書いている。以下はいずれも 2026-09-20 に c81574a で確かめた。origin/main はその後も進んでいるので、確かめ直すときは c81574a を指定してほしい。
1 つ目は仕様である。 openspec/specs/stripe-customer/spec.md の ### Requirement: Stripe 呼び出しはアダプタ境界に閉じ込める に次がある。
システムは、Stripe SDK の呼び出しをアダプタ層に隔離 SHALL。集金ドメイン(費目・期・価格・認可の判定)は Stripe 型に直接依存しては SHALL NOT。これにより「Stripe オフ→口座振込」や将来の API 移行をアダプタ差し替えで吸収できる。
これは openspec/changes/archive/ ではなく openspec/specs/ にある現行の仕様で、SHALL で書かれている。packages/api/src/stripe/client.ts の先頭コメントは、この要件を名前で引いている((stripe-customer spec: §Stripe 呼び出しはアダプタ境界に閉じ込める))。
2 つ目は design.md である。 「1. 概要と目的」に次がある。
原則: サークルが Stripe に依存せず、いつでも切り替えられること。 Stripe をオフにして口座振込を指示できるよう、ドメインと「人 ↔ Stripe オブジェクト」の対応表は 自前 DB を source of truth とし、Stripe は薄いアダプタ越しに使う。
同じ文書の「3. 全体アーキテクチャ」はより直接に述べている。図は ADP["Stripe アダプタ層"] を 1 つのノードとして置き、Stripe へ向かう辺を ADP --> STRIPE の 1 本だけにしている。図に続く本文は次のとおりである。
ポイントは Stripe アダプタ層 を境界にすること。ドメイン(誰がいくら払う/払い戻す)は Stripe 非依存に保ち、Stripe 呼び出しをアダプタに閉じ込めておけば、「Stripe オフ → 口座振込」への切り替えや、将来の v2 移行がアダプタ差し替えで済む。
同じ文書の「10. 将来: v2 移行の watch ポイント」にも次がある。
- ドメイン層を Stripe 非依存に保っておけば、移行は アダプタ差し替え で済む。
アダプタ層がどのディレクトリに当たるかは、仕様にも design.md にも書かれていない。 書いているのはアーカイブ済みの提案で、openspec/changes/archive/2026-06-19-add-membership-collection/proposal.md に次がある。
- Stripe アダプタ層の導入: Stripe SDK 呼び出しを
packages/api/src/stripe/のアダプタに閉じ込め、ドメイン層は Price/金額/期の判定のみを担う(Stripe オフ→口座振込 や将来の v2 移行をアダプタ差し替えで吸収)。
3 つ目は openspec/project.md である。
現状の Stripe 系エンドポイント(prices・products・invoices・checkout)は Stripe を allowlist で薄く包む管理・配管層であって、会員向けのドメイン API ではない。
仕様・design.md・提案の 3 つを合わせると、packages/api/src/stripe/ がアダプタ層であり、Stripe SDK の呼び出しはそこに隔離される。project.md を読むと、その外にある 4 つのルーター自体が「Stripe を薄く包む層」である。 どちらが今の方針かによって、後述の「アダプタの外に出ている」が逸脱なのか意図なのかが変わる。
これは記録の欠落ではなく、記録どうしの食い違いである。 仕様・design.md の 3 箇所・提案・client.ts の仕様参照は、いずれも 71dced4(2026-06-19)で入っている。project.md の上の記述を書いたのは 31a9954(2026-08-04、PR #38)で、4 つのルーターを入れたのと同じコミットである。 つまり 4 つの置き場所は意図して決められ、その意図は project.md に書かれている。新しいほうが project.md で、仕様は SHALL のまま残っている。
「以後変わっていない」は、後続のコミットを挙げて確かめた。 git log --oneline -- design.md が返すのは b33362c(2026-09-19、#77)と 71dced4 の 2 件で、b33362c が design.md で変えたのは mermaid の MAIL[…] の 1 行だけである(git show b33362c -- design.md)。git log --oneline -- openspec/specs/stripe-customer/spec.md が返すのは 71dced4 の 1 件だけである。git log --oneline -- openspec/project.md は 31a9954 のあとに 628a3ce(#40)と 9e8ddb0 の 2 件を返すが、git show 628a3ce -- openspec/project.md と git show 9e8ddb0 -- openspec/project.md のどちらも、上の引用の行に触れていない。git log -S は出現行数を変えない編集を返さないので、後続コミットの有無はこの形で確かめる必要がある。
どの記録で読んでも偽になる記述が 1 つある
packages/api/src/index.ts が ./stripe を再エクスポートする行に、次のコメントが付いている。
Stripe adapter layer (all Stripe SDK usage is isolated here).
SDK のハンドルは Context の stripe.sdk ゲッターから届く。これを呼んでいるファイルは次のコマンドで拾える。
git grep -ln '\.sdk\.' -- 'packages/api/src' ':!*.test.ts'
返るファイルには packages/api/src/stripe/ の外のものが含まれる。checkout.ts・invoices.ts・prices.ts・products.ts が context.stripe.sdk.prices.list(...) のように SDK を直に呼んでいる。
仕様は「Stripe SDK の呼び出しをアダプタ層に隔離 SHALL」、design.md は「Stripe 呼び出しをアダプタに閉じ込めておけば」と、いずれも隔離の対象を呼び出し全体としている。この広さで読むと all Stripe SDK usage is isolated here は成り立たない。 隔離の対象を狭く書いた例もリポジトリにはあり、packages/api/src/stripe/client.ts の先頭コメントは Stripe SDK initialization, isolated to the adapter layer. と初期化に限っている。ただし index.ts の行は all と書いているので、この狭い読みには当たらない。
SDK の呼び出しについては、逸脱しているのはこの 4 つのルーターだけである。 集金ドメインの側は SDK を直に呼んでいない。openspec/specs/membership-billing/spec.md の ### Requirement: 請求書の作成・確定・送付 は「請求書はドメイン非依存のアダプタ越しに作成する。」と課しているが、これを実装している packages/api/src/membership/router.ts の 3 つのプロシージャ(issueInvoice・issueSpecialInvoice・issueSpecialInvoiceByEmail。後ろの 2 つは同じ補助関数を共有している)は、いずれも packages/api/src/stripe/invoices.ts の createDraftInvoice を呼ぶ形になっていて、上のコマンドの返り値に membership/router.ts は含まれない。したがって、SDK の呼び出しを動かす対象は 4 つのルーターに限られる。 同じ要件の 2 文目に反する範囲は別である。 冒頭に引用した段落の 2 文目、集金ドメイン(費目・期・価格・認可の判定)は Stripe 型に直接依存しては SHALL NOT のほうで、次の節で述べるとおり packages/api/src/payments/normalize.ts が Stripe の型をインポートしている。ただし、この文が括弧で挙げている 4 つのどれにも normalize.ts は当たらない。 normalize.ts がしているのは Stripe のオブジェクトから PaymentRow への変換で、費目・期・価格・認可の判定ではない。この括弧が網羅なのか例示なのかを仕様が書いていないので、normalize.ts が「集金ドメイン」に当たるかは決まらない。下の判断に含まれる。
./stripe の外に呼び出しがあるという 事実 は、下の判断を待たずに確かめられる。ただし index.ts に何と書くかは待つ。 この行は仕様・design.md・提案と同じ決定を述べたもので、コメントだけを書き換えると仕様との食い違いが残り、SHALL から外れた状態を正常として記録することになる。
このパターンは client.sdk; s.invoices…、client["sdk"]、const { sdk: api } = client、stripe.sdk?.invoices、.sdk の直後で改行する形のいずれも拾わない。 そこで、大文字小文字を区別しない git grep -inE 'sdk' -- 'packages/api/src' ':!*.test.ts' の返す行を全て読んで裏付けた。.sdk. の形でない一致は、packages/api/src/stripe/client.ts のゲッターの宣言 get sdk(): Stripe { と、packages/api/src/test-utils.ts が Context を組み立てる { sdk: stripe } と、残りはすべて説明のコメントで、呼び出しは 1 つも含まれない。packages/api/src の外は git grep -inE 'sdk' -- . ':!pnpm-lock.yaml' ':!*.test.ts' ':!packages/api/src' で見た。拡張子で絞ると設定ファイルやスクリプトを取りこぼすので、絞らずに走査している(#89)。一致した 11 ファイルのうち .ts は 3 つで、いずれも説明のコメントの中で SDK に言及しているだけである。残る 8 つは .md の文書である。呼び出しは 1 つも無く、git grep -ln '\.sdk\.' -- . ':!pnpm-lock.yaml' ':!packages/api/src' は 0 件、git grep -n 'new Stripe(' -- . ':!pnpm-lock.yaml' が返すのは packages/api/src/stripe/client.ts の 1 行だけである。なお packages/api/src/**/*.ts のようにパスを絞ると packages/api/src の直下が外れ、上の 4 つが消える。
型のインポートは実行時の依存ではない
stripe をインポートしている箇所は次で拾える。
git grep -nE "from 'stripe'" -- '*.ts'
返る行のうち値としてインポートしているのは packages/api/src/stripe/client.ts の import Stripe from 'stripe' だけで、残りはすべて import type Stripe from 'stripe' である。テストを除くと、packages/api/src/stripe/ の外で型をインポートしているのは上の 4 つと packages/api/src/payments/normalize.ts である。
import type は実行時に消えるので、これは実行時の依存ではない。 normalize.ts の先頭のコメントは、これが意図した形だと書いている。
They accept the Stripe object types (so callers stay type-checked) but only read plain fields and OUTPUT plain DTOs — they never call the Stripe SDK. That keeps
payments/importable without pulling in any Stripe runtime, while the adapter (stripe/listing.ts) remains the only place that talks to Stripe.
前半が成り立つのは、packages/api/src/payments/index.ts が ./types・./dashboard・./pagination・./normalize の 4 つだけを再エクスポートし、./router を再エクスポートしていないからである。 payments/ の中で ../stripe をインポートしているのは payments/router.ts(と、それをモックする payments/router.test.ts)だけである(git grep -n '\.\./stripe' -- packages/api/src/payments)。packages/api/src/stripe/index.ts は export * from './client' を含み、client.ts は import Stripe from 'stripe' という値のインポートをするので、payments/router.ts をインポートすれば Stripe のランタイムはモジュールグラフに入る。 前半が言えるのは、payments/ の入口である index.ts がその router.ts を再エクスポートしていないからであって、payments/ の全体が Stripe のランタイムから切れているからではない。
型のインポートが仕様の Stripe 型に直接依存しては SHALL NOT に反するかどうかは、下の判断に含まれる。ただしこの文の後半には、それとは別の問題がある。 the only place that talks to Stripe の主語が payments/ なのかリポジトリ全体なのかで正誤が変わる。前者なら成り立つ。payments/ から Stripe を呼び出す経路は payments/router.ts の import { listCheckoutSessions, listInvoices } from '../stripe' だけで、この 2 つは packages/api/src/stripe/listing.ts にある(git grep -nE 'export (async )?function (listInvoices|listCheckoutSessions)' -- packages/api/src で確かめた)。後者なら成り立たない。stripe/listing.ts のほかにも stripe/connect.ts・stripe/customers.ts・stripe/invoices.ts・stripe/transfers.ts・stripe/webhook.ts が SDK を呼んでいる。範囲を明示して曖昧さを消すことは、下の判断を待たずにできる。
このパターンは、二重引用符の from "stripe"、stripe/... のサブパス、import('stripe')、require('stripe') を拾わない。そこで、この 4 形をすべて含む git grep -nE "(from|import\(|require\() *['\"]stripe" -- '*.ts' でも数えた。-E を落とすとこのパターンは括弧が不均衡だとして失敗する(fatal: command line, ... parentheses not balanced)ので、フラグごとこの形で実行する。広いほうも同じ行数を返すが、同じ行に 2 つの形が同居していれば行数は変わらないので、行数の一致では 0 件を示せない。 返った行を全て読み、いずれも import [type] Stripe from 'stripe' の形だったことで確かめた。
誰が何を誤るか
仕様と design.md だけを読んだ人が、Stripe SDK の呼び出しがアダプタ層に隔離されていると読む。仕様は SHALL なので、読んだ人はそれを満たすべき制約として扱う。 実際には呼び出しが packages/api/src/stripe/ の外にあり、project.md はそれを意図した形として書いている。どの記録を読んだかで、設計の理解と、今の状態が逸脱かどうかの判断が食い違う。
切り替えの費用についても、仕様と design.md だけを読むと過小に見積もる。ただし project.md は、移植性を目的にしていないことを別の箇所で明言している。
フィールドの語彙は Stripe に合わせるが、決済プロバイダの移行容易化は目的にしない(移行時は入力語彙・カーソル・ハンドラの書き換えも要るため、allowlist だけでは移植性は得られない)。
design.md が「いつでも切り替えられること」を原則として掲げることと、この行が「決済プロバイダの移行容易化は目的にしない」と述べることは、同時には成り立たない。 前者を目指す状態、後者を現在の状態として読めば両立するが、どちらの文書もその関係を書いていない。どちらが今の方針かも、下の判断に含まれる。
直さずに残した理由
3 つの記録のどれを正とし、残りをどう直すかの判断が要る。 openspec/project.md は「あらゆる変更は OpenSpec の提案(/opsx:propose)として始まり → 適用(/opsx:apply) → アーカイブ(/opsx:archive) される」と定めているので、仕様を直す場合は design.md と project.md を直すのとは手順も費用も違う。 取りうる手は少なくとも次の 3 つである。
- 仕様を正とする。
packages/api/src/stripe/の外にある 4 つのルーターを仕様に合わせて動かすか、この 4 つを仕様の例外として仕様の側に明記する。前者はコードの移動を伴い、後者はproject.mdの記述をその例外に合わせる必要がある。 project.mdを正とする。 仕様のSHALLを今の形に合わせて書き換える。この変更は/opsx:proposeから始める必要がある。- 役割を分けて両立させる。 仕様と
design.mdを目指す状態、project.mdを現在の状態と位置付け、その関係を明記する。仕様の本文は変えずに済むが、SHALLを「まだ満たしていない要件」として扱ってよいかは、OpenSpec の運用の側の判断になる。
加えて、packages/api/src/stripe/ の外にある 4 つのルーターは #53 の対象で、#53 が取りうる手の先頭に挙げているのは「画面から呼ばれていない prices・products・invoices・checkout を契約から外す」である。#53 はその結論が #15 の決める認可の形に依存すると書いている。#53 と #15 が決まる前に 4 つを動かすと、決まった後で作り直しになる。
上に挙げたうち、normalize.ts の一文の範囲を明示することだけが、この判断を待たない。 index.ts の断定は、./stripe の外に呼び出しがあるという事実の確認は待たないが、代わりに何と書くかが記録の側の決定に依存するので待つ。
normalize.ts を直すときは #87 と合わせて行うこと。 #87 は同じファイルの別のコメント(invoiceToRow の中にある Stripe's types declare で始まるもの)について、payments を他の 2 つと分けて書くことを求めている。本 issue が対象にしているのはファイル先頭のコメントで、別の行である。#87 は「そのブランチは normalize.ts を触っておらず、触ると変更の範囲が広がる」として保留しているので、別々に着手すると同じファイルを 2 回に分けて触ることになる。
- Lingua principale
- TypeScript
- Stelle
- 0
- Fork
- 0
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
- Include un Dockerfile o un file Docker Compose
- Nessun modello di pull request
- Nessuna guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di traPtitech/Checkin
-
bug documentation
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 90/100
traPtitech/Checkin#87 ·
-
documentation
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 95/100
traPtitech/Checkin#64 ·
-
enhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
traPtitech/Checkin#59 ·
-
bug
Difficoltà 5/5 Più di una settimana Idoneità per principianti 30/100
traPtitech/Checkin#92 ·
-
enhancement
Difficoltà 5/5 Più di una settimana Idoneità per principianti 35/100
traPtitech/Checkin#91 ·
Tutte le issue di traPtitech/Checkin
Issue simili
-
category::task code priority::normal
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
e0ipso/strikethroo#145 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 1/5 1-3 ore Idoneità per principianti 85/100
cocos/cocos-docs#3333 ·
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 75/100
RevenueCat/react-native-purchases#2079 ·
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
RevenueCat/purchases-hybrid-common#2039 ·
I maintainer di solito rispondono entro 1 giorno
-
agent-reported area/react bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
I maintainer di solito rispondono entro 1 giorno