# デモショップ 設計書

テストデータの作成（`data generate --strategy llm --reference design:設計書/デモショップ設計書.md`）で AI が読む設計書です。テストの前提をどう用意するかは、ここに書いた業務ルールと API から決まります。

## 構成

- 画面と JSON API: `sut/server.mjs`（既定で `http://127.0.0.1:4173`）
- gRPC: `sut/grpc.mjs`（既定で `127.0.0.1:4174`、定義は `proto/shop.proto`）
- データはすべてメモリ上にあり、起動し直すと初期状態に戻る

## 業務ルール

| ID | ルール |
| --- | --- |
| BR-01 | 起動時の会員は `member@example.com`（デモ会員、パスワード `demo-pass-1`）と `admin@example.com`（デモ管理者、パスワード `demo-pass-2`）の 2 人。この 2 人は削除できない |
| BR-02 | 起動時の商品は `sut/data/products.json` の 8 件で、ファイルは書き換えない。在庫 0 の商品は P-004（革のカードケース）。商品を増やすには取込（BR-08）を使う |
| BR-03 | カートはショップ全体で 1 つだけ。`POST /api/cart` に `{ "productId", "quantity" }` を送ると入る。数量は在庫数までに抑えられ、在庫 0 の商品は 409 になる |
| BR-04 | 小計が 5000 円以上なら送料 0 円、それ未満（0 円を除く）は送料 600 円 |
| BR-05 | `POST /api/reset` でカートを空に戻す。カートの中身に依存するテストは、はじめにこれを呼んでから必要な商品を `POST /api/cart` で入れる |
| BR-06 | ログインは `POST /api/session` に `{ "email", "password" }` を送る。成功すると 200 で会員名が返り、失敗すると 401 になる。セッションは保存しない |
| BR-07 | 会員登録は `POST /api/members` に `{ "email", "password", "name" }` を送る。メールアドレスの形でない、パスワードが 8 文字未満、名前が空なら 400、同じメールアドレスがあれば 409、成功すると 201。登録した会員はすぐにログインできる。`DELETE /api/members/{email}` で消せる（起動時の 2 人は 409） |
| BR-09 | `--db <SQLite ファイル>` を付けて起動すると DB モードになり、商品は `data/products.json` ではなく、そのファイルの `products` テーブルから出る。テーブルはショップが作らない。「データ設計」どおりの DDL で作り、商品は seed（INSERT）で入れてから使う。SQL は `sqlite3 <SQLite ファイル>` に標準入力で渡す。テーブルが無い間は商品一覧は空になる。取込（BR-08）は DB モードでは `products` テーブルへ書く。`/api/reset` は DB の商品には触れない |
| BR-08 | 商品取込は画面 `/import` でファイルを選んで取り込む。CSV は 1 行目が `id,name,price,stock` の見出し、XML は `<products><product><id/><name/><price/><stock/></product></products>`。同じ id の商品は上書きされる。読める商品が 1 件も無ければ 400。取り込めると「N 件を取り込みました」と出る |

## データ設計（DB モード、BR-09）

SQLite。テーブルは 1 つ。

| テーブル | 列 | 型 | 制約 | 説明 |
| --- | --- | --- | --- | --- |
| products | id | TEXT | PRIMARY KEY | `P-001` のような商品 ID |
| products | name | TEXT | NOT NULL | 商品名 |
| products | price | INTEGER | NOT NULL, 0 以上 | 税込価格（円） |
| products | stock | INTEGER | NOT NULL, 0 以上 | 在庫数 |

DDL は `CREATE TABLE IF NOT EXISTS`、seed は `INSERT ... ON CONFLICT(id) DO UPDATE` のように、何度流しても同じ結果になる形で書く。

## API

| メソッド | パス | 用途 |
| --- | --- | --- |
| GET | `/api/products?q=&sort=` | 商品の検索と並び替え（`sort` は `price-asc` / `price-desc`） |
| GET | `/api/cart` | カートの中身と金額 |
| POST | `/api/cart` | カートに入れる |
| DELETE | `/api/cart/{productId}` | カートから外す |
| POST | `/api/session` | ログイン |
| GET | `/api/members` | 会員の一覧（メールアドレスと名前） |
| POST | `/api/members` | 会員登録（BR-07） |
| DELETE | `/api/members/{email}` | 会員の削除（BR-07） |
| POST | `/api/products/import` | 商品の取込。本文は CSV か XML のテキスト、`content-type` は `text/csv` か `application/xml`（BR-08） |
| POST | `/api/reset` | カートを空にし、会員と商品を起動時の状態に戻す（BR-05） |
| GET | `/api/healthz` | 動いているかどうか |
| GET | `/api/slow?ms=` | 指定のミリ秒待ってから 200 を返す（最大 10 秒）。可用性テストで遅延を観測するためのもの |
