運用診断(Diagnostics)プラグイン¶
概要¶
DiagnosticsPlugin は、設定ミスが顧客の不利益(1 件も商品が表示されない・0 円販売・上代超え 等)を生んでいないかを運用者が一覧で発見し、該当画面へ飛んで対処するための汎用診断フレームワークです。価格・表示に限定せず、検査項目(check)をレジストリ登録式で追加できます。
価格計算(顧客×商品の掛率/SMILE 単価マスタ/Commercial Rule)と商品可視性(typed VisibilityRule)には入力バリデーション層が無く、設定ミスがそのまま顧客影響に直結します。本プラグインは実エンジンを実データに対して走らせ、不利益な「実結果」を検出します。
- 実装:
packages/plugins/src/diagnostics/ - React Dashboard 画面: 「設定 > ◇ 運用診断」(route
/diagnostics) - Admin API:
diagnosticChecks/runDiagnostics - 新 entity・DB migration は不要(既存エンジンの再利用のみ)
主要コンセプト¶
- DiagnosticCheck(検査項目):
id/name/description/run(ctx, options)を持つ provider。契約 SSoT はservice/diagnostic-check.ts。 - DiagnosticFinding(発見): 1 件の問題。
category/severity(error/warning/info)/title/detailと、対処先リンク用の任意 entity ref(customerId/productVariantId/collectionId/commercialRuleId/smilePriceMasterId/linkPath等)を持つ。customerCountは後述のグルーピングで畳み込んだ顧客数。 - DiagnosticsService(集約実行): 登録された全 check を順に実行し、各 check を try/catch で隔離(1 つが失敗しても他を止めない)。結果に
status(ok / issues_found / error)・durationMs・truncatedを付与してレポート化する。 - 顧客グルーピング: 価格・表示は実質「顧客分類(得意先分類)+顧客グループ+顧客ステータス」で決まるため、これらが同一の顧客は代表 1 件で評価し、評価回数を
顧客数×商品数からグループ数×商品数へ畳み込む(checks/shared/customer-grouping.service.ts)。
提供される検査項目¶
| check id | 表示名 | 検出内容 | 種別 | 対処先リンク |
|---|---|---|---|---|
zero-visibility-customers |
0 件可視の顧客 | ログインしてもどの商品も表示されない顧客(表示制御ルールで全 variant が非表示) | 動的 | 顧客詳細 /customers/$id |
invisible-campaigns |
誰にも表示されないキャンペーン | 現在有効なキャンペーンのうち、表示制御ルール上どの顧客にも表示されないもの。Storefront の campaign 連動バナーにも出ない状態 | 動的 | キャンペーン詳細 /commercial-rules/$id |
pricing-anomalies |
価格異常(0 円・上代超え) | 実価格計算で finalUnitPrice <= 0(0 円・負)、finalUnitPrice > listPrice(掛率>1)、計算失敗の組合せ |
動的 | 価格・販促シミュレーター /commercial-rules/simulator |
pricing-rule-config |
価格設定ミス(0 円ルール) | 価格ルールの set_unit_price:0 / multiply_unit_price:0、SMILE 単価マスタの 0 円行(定義レベル) |
静的 | 価格ルール詳細 / SMILE 単価マスタ |
private-brand-collections |
非公開のブランド配下コレクション | ブランド配下で isPrivate=true のコレクション。Shop API の collection tree から除外され、Storefront のブランドメニューに出ない |
静的 | コレクション詳細 /collections/$id |
missing-delivery-point-orders |
納品先コード未設定の通常受注 | 通常出荷(DIRECT 以外)の確定済み注文で Order.customFields.deliveryPointCode が未設定のもの。1 件でも残ると SMILE 受注 CSV エクスポートが失敗する |
動的 | 注文詳細 /orders/$id |
- 動的検査は
CustomerVisibilityService.hasAnyVisibleVariantとPriceCalculationService.calculateCatalogPriceを実データに対して実行し、「実際に顧客に適用される結果」で判定します。 missing-delivery-point-ordersは注文の納品先コードを自動補完・自動採番しません。新規注文は checkout で納品先コードを確定し、過去の未設定注文は運用者が Vendure Dashboard の注文詳細で SMILE 取込済みコードを手動設定します。invisible-campaignsはcommercial_rule_entity.kind = campaignかつ現在有効なルールだけを対象にし、typed VisibilityRule のVisibilityRuleService.resolve()で顧客代表バケットごとに campaign target を実判定します。未来開始・終了済み・無効 campaign は異常扱いしません。- 静的検査は実適用の有無に関わらず「0 円を生み得る設定」を定義走査で surface し、まだ顧客に当たっていない潜在的なミスも拾います。
private-brand-collectionsは意図的な非表示設定もあり得るためwarningとして表示します。商品可視性ルールで商品が許可されていても、Vendure のisPrivateは Shop API の collection tree からそのコレクションを落とすため、ブランドメニューの項目数確認に使います。
ブランドメニュー表示条件 matrix¶
Storefront のブランドメニューは、Vendure Shop API の collections から brands root 配下の collection tree を作り、その後 visibleCollectionIds で顧客別に再帰フィルタします。isPrivate は Shop API の collection tree 取得時点で除外されるため、商品可視性ルールとは別の表示条件です。
| Vendure collection の状態 | Shop API collections の tree |
顧客別 visibleCollectionIds / 可視商品 |
Storefront ブランドメニュー | private-brand-collections の表示 |
読み方 |
|---|---|---|---|---|---|
brands 配下で isPrivate=false |
tree に含まれる | 自身、または子孫に可視商品がある | 表示される | 表示しない | 通常の表示条件。 |
brands 配下で isPrivate=false |
tree に含まれる | 自身にも子孫にも可視商品がない | 表示されない | 表示しない | 非公開設定ではなく、商品可視性・商品紐付け・検索 index 側を確認する。 |
brands 配下で isPrivate=true |
tree から除外される | 可視商品がある | 表示されない | warning として表示する |
商品は見えるのにメニューだけ少ない典型例。意図的に隠す用途でなければ非公開を解除する。 |
brands 配下で isPrivate=true |
tree から除外される | 可視商品がない | 表示されない | warning として表示する |
非公開設定自体は警告する。メニューに出す予定なら、非公開解除後に可視性・商品紐付けも確認する。 |
brands 配下の祖先 collection が isPrivate=true |
private 祖先が tree から除外され、子孫も brands tree に接続できない |
子孫に可視商品がある場合でも menu tree に載らないことがある | 表示されない | private 祖先を warning として表示する |
子 collection が公開でも、親が非公開なら階層導線が切れる。 |
brands 配下ではない collection |
ブランド menu の対象外 | 任意 | ブランドメニューには表示しない | 表示しない | キャンペーン・商品タイプなど別 root の導線として扱う。 |
アーキテクチャと拡張方法¶
packages/plugins/src/diagnostics/
├── index.ts # DiagnosticsPlugin(VisibilityPlugin / CommercialRulesPlugin を import)
├── api/
│ ├── schema.ts # Admin GraphQL schema
│ └── diagnostics.admin.resolver.ts
├── service/
│ ├── diagnostic-check.ts # DiagnosticCheck / DiagnosticFinding / DIAGNOSTIC_CHECK トークン(契約 SSoT)
│ └── diagnostics.service.ts # 集約実行・レポート化
└── checks/
├── shared/
│ ├── customer-grouping.service.ts # 顧客 signature 畳み込み
│ └── finding-helpers.ts # 顧客ラベル・金額整形
├── zero-visibility-customers.check.ts
├── invisible-campaigns.check.ts
├── missing-delivery-point-orders.check.ts
├── pricing-anomalies.check.ts
├── pricing-rule-config.check.ts
└── private-brand-collections.check.ts
新しい検査項目を追加する手順:
DiagnosticCheckを実装したクラスをchecks/に追加する(runでDiagnosticFinding[]を返す。打ち切る場合はtruncatedを必ず設定)。index.tsのprovidersにクラスを登録し、DIAGNOSTIC_CHECKトークンのuseFactory/inject配列に加える。- 追加後は Dashboard の診断ページに自動で項目が現れる(UI 改修不要)。
// 例: 新 check の骨子
@Injectable()
export class MyCheck implements DiagnosticCheck {
readonly id = "my-check";
readonly name = "私の検査";
readonly description = "…を検出します。";
async run(ctx: RequestContext, options: DiagnosticCheckOptions): Promise<DiagnosticCheckOutput> {
return {
findings: [
/* … */
],
};
}
}
Admin GraphQL API¶
extend type Query {
"登録されている診断 check の一覧を返す。"
diagnosticChecks: [DiagnosticCheckInfo!]!
"診断を実行する。checkIds 未指定なら全 check。limit は走査対象(商品数など)の上限ヒント。"
runDiagnostics(checkIds: [String!], limit: Int): DiagnosticReport!
}
- 権限:
AuthenticatedかつUpdateCatalog。 DiagnosticReport:results(check ごとのDiagnosticCheckResult)・totalFindings・ranAt。DiagnosticCheckResult:status/findings/truncatedReason/omittedCount/errorMessage/durationMs。
CLI / API での実行¶
Dashboard を開かずに端末から診断結果を把握する標準入口を用意しています(READ-ONLY のため backup guard 不要)。
just diagnostics # local: 全 check を実行(text 出力)
just diagnostics local 200 # 商品走査上限 200
just diagnostics-json local 50 # JSON 出力(AI / スクリプト連携向け)
just diagnostics-json staging 50 # staging: URL と secrets config を deploy target から自動解決
just diagnostics-list # 登録済み check 一覧
- 実体は
scripts/ops/run-diagnostics.mjs。Admin API へログイン(vendure-auth-token)しrunDiagnostics/diagnosticChecksを叩く。 - local の認証・接続先は
apps/vendure-server/.env(SUPERADMIN_USERNAME/SUPERADMIN_PASSWORD/PORT)を既定値に使う。 - staging / production を
just diagnostics* <env>から実行する場合は、deploy-targets.shから Admin API URL とSECRETS_CONFIG(staging_vendure/production_vendure)を自動解決してwith-env経由で認証情報を読む。手元で上書きする場合だけ、第 4 引数以降のconfig/url/origin、またはRITSUBI_ADMIN_API_URL/SUPERADMIN_USERNAME/SUPERADMIN_PASSWORDを使う。 node scripts/ops/run-diagnostics.mjsを直接叩く場合、staging / production では--url(またはRITSUBI_ADMIN_API_URL)と認証情報(SUPERADMIN_USERNAME/SUPERADMIN_PASSWORD)を環境変数 / フラグで渡す(リポジトリに埋め込まない)。- 終了コード: 検出 0 件=0、finding あり=1、check に error=2、接続/認証等の失敗=3。
- 生の Admin API を直接叩く場合は、状態変更(mutation の login)に
Originヘッダが必要(origin allowlist はCORS_ORIGIN/STOREFRONT_URL/ADMIN_URL由来)。
React Dashboard 上の見え方¶
「設定 > ◇ 運用診断」を開くと自動で全 check を実行します(重い処理のため focus/再マウントでは再実行せず、最新化は「再実行」ボタン)。check ごとにセクション表示し、各 finding は severity アイコン・件数・説明と「対処 →」リンク(顧客 / 価格ルール / SMILE 単価マスタ / シミュレーター)を持ちます。◇ は自作 extension marker(AGENTS 規約)です。
画面で「You are not currently authorized」が出る場合はログインセッション切れ(backend 再起動で bearer トークンが無効化)です。再ログイン(またはハード再読み込み)で解消します。権限判定は
AuthenticatedORUpdateCatalogで、ログインが有効なら通ります。
運用上の注意・既知の制限¶
- オンデマンド実行・同期処理: 動的検査は顧客×商品の実計算を伴うため、ボタン実行時に同期で走ります。グルーピングで評価回数を畳み込みますが、大規模カタログでは応答に時間がかかります。
- 走査上限と no-silent-cap:
pricing-anomaliesは商品数(既定 800)と finding 数(1000)、pricing-rule-configの SMILE 行(既定 500)に上限があります。打ち切った場合はtruncatedReasonとomittedCountで必ず明示し、黙って打ち切りません。limit引数で商品上限を上書きできます。 - グルーピングの前提: 表示制御の
customer条件・subjectSet の個別 item・SMILE 得意先別単価行(priceKindCode=2)で名指しされた顧客は個別評価に分離します。一方、請求先継承による subjectSet マッチの間接的差異までは畳み込みで吸収しません(過小グルーピングを避け、過大グルーピングのみ許容する設計)。 - 責務分界: 検査ロジックの正本は backend(
DiagnosticsPlugin)です。Dashboard 画面は実行と表示のみを担います。
関連ドキュメント¶
- 価格計算ロジック: pricing/price-calculation.md / pricing-system.md
- 商品可視性: customer-visibility.md
- SMILE 単価マスタ: smile-integration.md / smile/smile-master-data-reference.md