顧客登録・検索
顧客登録・検索
Section titled “顧客登録・検索”Document ID: SRS-CUS-001 Parent: SRS-ROOT-001 v0.5 Version: 0.2 Status: Implemented (API + Web UI 最小) Last Updated: 2026-05-06 Depends on: SRS-TEN-001 v0.4, SRS-TEN-002 v0.2, SRS-TEN-003 v0.2, SRS-MST-001 v0.6 依存される: SRS-CUS-002, SRS-RES-002 v0.5, SRS-RES-003
本書は SRS-ROOT-001 v0.5 に従う。
店舗オペレーターが顧客を新規登録し、店舗内で高速に検索できるようにする。
Phase 1 では name と name_kana を最小必須入力とし、電話番号は E.164 で保存、重複検出は phone_e164 完全一致 warning のみで扱う。顧客統合 UI は Phase 2 以降の SRS-CUS-003 に委譲する。CSV 一括インポートは別 SRS-CUS-004(注: link 管理 SRS と番号衝突する場合は SRS-CUS-005 などへ振り直す。本 Phase 1 では SRS-CUS-004 は operator_staff_link を所有しているため、CSV インポートは Phase 2 に持ち越す形で OQ-CUS-001-CSV を残す)。
2. ユーザーストーリー
Section titled “2. ユーザーストーリー”- As a 受付, I want to 来店中に最小入力で顧客を素早く作成したい, so that 電話・店頭対応を止めずに予約へ進める
- As a 店長, I want to カナで顧客検索したい, so that 同音異字の顧客も取りこぼさない
- As a 受付, I want to 同じ電話番号の既存顧客がいたら警告してほしい, so that 重複作成を減らせる
- As a 店長, I want to 店舗内通番の顧客番号を持たせたい, so that 受付票や会員証の運用に使える
3. ユースケース
Section titled “3. ユースケース”3.1 主シナリオ
Section titled “3.1 主シナリオ”- オペレーターが「お客様管理 > 新規登録」を開く
nameとname_kanaを入力する- 任意で
phone,email,birthday,gender,occupation,post_mail_opt_in,preferred_staff_idを入力する - サーバが
phoneを E.164 へ正規化する - トランザクション内で店舗単位 advisory lock を取得し、
store_settings.customer_no_seqを+1してcustomer_noを採番する customerを INSERT するphone_e164完全一致の既存顧客があれば warning を返す- 一覧に遷移し、作成した顧客を表示する
3.2 代替フロー
Section titled “3.2 代替フロー”- AF-1 電話番号なしで登録する: 保存可。重複検出は行わない
- AF-2 予約作成ダイアログからインライン新規作成: API 契約は同一、UI だけ簡略化
- AF-3
preferred_staff_idだけ設定: スタッフ未リンクのオペレーターでも保存可 - AF-4 PII 閲覧権限を持たないオペレーターが検索: 顧客は検索できるが
phone/email/birthdayはマスクされる(Phase 1 ではadmin:customer:read内部の表示制御で実装、read_pii_sensitive別 key は Phase 2)
3.3 例外フロー
Section titled “3.3 例外フロー”- EF-1
name空文字: 422CUSTOMER.INPUT_INVALID - EF-2
name_kana空文字: 422CUSTOMER.INPUT_INVALID - EF-3
phoneが E.164 正規化不能: 422CUSTOMER.PHONE_INVALID - EF-4
preferred_staff_idが同店舗の現役 staff でない: 422CUSTOMER.PREFERRED_STAFF_INVALID - EF-5 他店舗顧客検索・更新試行: RLS により 404
- EF-6 削除済み顧客は既定検索に出さない
- EF-7 advisory lock 取得失敗: 503
CUSTOMER.RETRYまたは backoff retry
4. UI仕様
Section titled “4. UI仕様”4.1 画面の目的
Section titled “4.1 画面の目的”- 顧客一覧検索画面
- 顧客新規登録ダイアログ / 単独画面
- 予約作成導線からの簡易顧客作成
4.2 主要要素
Section titled “4.2 主要要素”- 検索条件:
q,phone,customer_no,preferred_staff_id - 一覧列:
customer_no_display,name,name_kana,preferred_staff_name,created_at - 新規登録フォーム: 必須
name,name_kana - 任意項目:
phone,email,birthday,gender,occupation,post_mail_opt_in,preferred_staff_id - 重複警告バナー: 「同一電話番号の顧客が存在」
4.3 バリデーション
Section titled “4.3 バリデーション”name: 1..200、trim 後空不可name_kana: 1..200、trim 後空不可(zod regex でカタカナ・ひらがな・スペース許可)phone: E.164 正規化後保存gender:female | male | unspecifiedoccupation: enum (student | employee | part_time | homemaker | self_employed | other)post_mail_opt_in: boolean
5. API仕様
Section titled “5. API仕様”5.1 エンドポイント一覧
Section titled “5.1 エンドポイント一覧”| Method | Path | 用途 | permission |
|---|---|---|---|
| GET | /api/admin/customers | 顧客検索一覧 | admin:customer:read |
| POST | /api/admin/customers | 顧客新規作成 | admin:customer:create |
| GET | /api/admin/customers/{id} | 顧客サマリ取得 | admin:customer:read |
5.2 zodスキーマ
Section titled “5.2 zodスキーマ”GET /api/admin/customers query:
q?: string(name / name_kana に pg_trgm GIN 検索)phone?: string(E.164 正規化後 exact match)customer_no?: stringpreferred_staff_id?: uuidcursor?: stringlimit?: 1..100 default 50
POST /api/admin/customers request:
name: stringname_kana: stringphone?: stringemail?: stringbirthday?: YYYY-MM-DDgender?: 'female' | 'male' | 'unspecified'occupation?: enumpost_mail_opt_in?: booleanpreferred_staff_id?: uuid | null
response:
data.customer.iddata.customer.customer_nodata.customer.customer_no_display(6 桁ゼロパディング)data.customer.namedata.customer.name_kanawarnings?: [{ code: 'CUSTOMER.DUPLICATE_PHONE', duplicate_customer_ids: uuid[] }]
5.3 エラーコード
Section titled “5.3 エラーコード”CUSTOMER.INPUT_INVALIDCUSTOMER.PHONE_INVALIDCUSTOMER.PREFERRED_STAFF_INVALIDCUSTOMER.FORBIDDENCUSTOMER.NOT_FOUND
6. データモデル影響
Section titled “6. データモデル影響”6.1 スキーマ
Section titled “6.1 スキーマ”customer(placeholder を昇格)
Section titled “customer(placeholder を昇格)”| カラム | 型 | 制約 |
|---|---|---|
id | uuid | PK |
store_id | uuid | NOT NULL, FK |
customer_no | bigint | NOT NULL |
name | varchar(200) | NOT NULL, CHECK btrim(name) <> '' |
name_kana | varchar(200) | NOT NULL, CHECK btrim(name_kana) <> '' |
phone_e164 | varchar(20) | NULL |
email | varchar(255) | NULL |
birthday | date | NULL |
gender | varchar(20) | NOT NULL DEFAULT ‘unspecified’, CHECK IN ('female','male','unspecified') |
occupation | varchar(30) | NULL |
post_mail_opt_in | boolean | NOT NULL DEFAULT false |
preferred_staff_id | uuid | NULL, 複合 FK → staff(store_id, id) |
version | integer | NOT NULL DEFAULT 1 |
created_at | timestamptz | NOT NULL |
updated_at | timestamptz | NOT NULL |
deleted_at | timestamptz | NULL |
| 制約 | UNIQUE(store_id, id) | |
| 制約 | UNIQUE(store_id, customer_no) | |
| INDEX | GIN (name_kana gin_trgm_ops) | |
| INDEX | GIN (name gin_trgm_ops) | |
| INDEX | (store_id, phone_e164) | |
| INDEX | (store_id, preferred_staff_id, created_at DESC) | |
| INDEX | (store_id, deleted_at) (90 日 purge job 用) |
6.2 マイグレーション計画
Section titled “6.2 マイグレーション計画”0018_customer_crm.sql で customer placeholder を昇格 + customer_note, customer_tag, customer_tag_link (CUS-002) も同時導入。
pg_trgm extension は bootstrap で有効化(infra/sql/bootstrap/001_roles_and_extensions.sql)。
customer_no_seq は SRS-TEN-001 v0.4 の 0011 で既に追加済み。
permission backfill (admin:customer:read/create) も 0018 で実施。
7. 業務ルール
Section titled “7. 業務ルール”7.1 customer_no 採番
Section titled “7.1 customer_no 採番”- 店舗単位 advisory lock:
pg_advisory_xact_lock(hashtext('customer_no:' || store_id)) - ロック取得後
store_settings.customer_no_seq + 1を取得し UPDATE - INSERT 時
customer.customer_noにセット - 表示は 6 桁ゼロパディング(
'000001'等)。bigint なので 999999 超えても運用継続可(表示は 7 桁以上に拡張)
7.2 phone 正規化
Section titled “7.2 phone 正規化”- 入力
09012345678→ 保存+819012345678 - 入力
+819012345678→ 保存 そのまま - 不可解な形式 →
CUSTOMER.PHONE_INVALID
7.3 重複検出
Section titled “7.3 重複検出”phone_e164完全一致の active 顧客があれば warning- 自動 merge は実施しない(Phase 2 SRS-CUS-003)
7.4 検索
Section titled “7.4 検索”qパラメータはname/name_kanaの trgm GINphoneは exact matchcustomer_noは数値完全一致
7.5 削除(親 §6.4 / §7.5)
Section titled “7.5 削除(親 §6.4 / §7.5)”- 削除は
deleted_atのみ - 90 日経過後の物理削除ジョブ
cleanup-deleted-customers-for-storeは SRS-WRK-001 で起票
7.6 監査
Section titled “7.6 監査”customer.create / updatecustomer.delete(CUS-002 から)- 検索 GET は通常監査なし(個別の sensitive read は CUS-002 の
read_pii_sensitive系で議論)
8. 非機能要件
Section titled “8. 非機能要件”- 顧客検索 API p95 300ms 以下
name_kanaGIN は 10 万件まで効く想定
9. セキュリティ・認可
Section titled “9. セキュリティ・認可”admin:customer:read: 全 roleadmin:customer:create: 全 role(受付・スタッフが現場で登録)- RLS は
store_id強制 - PII 表示マスキングは Phase 1 では未実装(
admin:customer:read単一)
10. 受け入れ基準(Given-When-Then)
Section titled “10. 受け入れ基準(Given-When-Then)”- GWT-1: Given
nameとname_kanaを入力 / When 保存 / Then 顧客が作成されcustomer_no_displayが 6 桁で返る - GWT-2: Given 同店舗に同一
phone_e164の顧客 / When 新規作成 / Then 作成成功しCUSTOMER.DUPLICATE_PHONEwarning が返る - GWT-3: Given 削除済み顧客 / When 既定の一覧検索 / Then 含まれない
- GWT-4: Given operator が
operator_staff_link無し / Whenadmin:customer:createで登録 / Then 正常保存(self-scope は適用しない) - GWT-5: Given カナ検索
'タナカ'/ When pg_trgm GIN 検索 / Then 該当顧客が返る - GWT-6: Given
customer_no_seq=42/ When 新規作成 / Thencustomer_no=43で保存されcustomer_no_display='000043' - GWT-7: Given 別店舗のセッション / When 顧客取得 / Then 404
- GWT-8: Given
phone='abc'/ When 保存 / Then 422CUSTOMER.PHONE_INVALID
11. テスト計画
Section titled “11. テスト計画”- Unit: phone 正規化、customer_no 表示、kana trim
- Integration: duplicate warning, RLS, advisory lock concurrency
- Contract: zod schema
12. 関連ジョブ
Section titled “12. 関連ジョブ”cleanup-deleted-customers-for-store(SRS-WRK-001)
13. Open Questions
Section titled “13. Open Questions”| # | 内容 | 扱い |
|---|---|---|
| OQ-CUS-001-01 | occupation enum vs free text | Phase 1 enum、Phase 2 検討 |
| OQ-CUS-001-02 | 電話番号 suffix match を許すか | Phase 1 exact、Phase 2 検討 |
| OQ-CUS-001-03 | PII マスキング (read_pii_sensitive) を Phase 1 で出すか | Phase 2(OQ) |
| OQ-CUS-001-CSV | CSV 一括インポート (本家互換、SRS-CUS-005 仮) | Phase 2 へ持越し |
14. 変更履歴
Section titled “14. 変更履歴”| Version | Date | Author | Change |
|---|---|---|---|
| 0.1 | 2026-05-05 | Codex / yudai | 初版ドラフト |
| 0.2 | 2026-05-05 | yudai (with Codex co-design) | Round 2 反映: migration 番号 0018 確定、name + name_kana 必須、customer_no_seq の advisory lock 採番ロジック確定、operator_staff_link は self-scope に効かないと明記、CSV インポートは Phase 2 OQ-CUS-001-CSV へ持越し |