← ドキュメント一覧
Doc 03

ER図

Entity-Relationship Diagram

データモデル設計方針

本システムのデータモデルは 10 エンティティ で構成されます。 マスタ系(静的データ)・トランザクション系(業務データ)・締め系(日次運用データ)の 3 層に分類し、 フロントエンドの Jotai AppStore と バックエンドの Cloudflare D1 データベースの両方を意識した設計です。

マスタ系
CATEGORY · PRODUCT
EMPLOYEE · TABLE
4 エンティティ
トランザクション系
VISITOR · ORDER
ORDER_ITEM · BILL · BILL_ITEM
5 エンティティ
締め系
DAILY_CLOSE
1 エンティティ(運用)

全体ER図

全 10 エンティティのリレーションシップ。Mermaid.js によるレンダリング。

erDiagram
    CATEGORY {
        int       id          PK
        string    name
        string    slug        UK
    }
    PRODUCT {
        int       product_id  PK
        int       category_id FK
        string    product_name
        int       price
        string    description
        boolean   is_active
        string    image_url
        string    created_at
        string    updated_at
    }
    EMPLOYEE {
        int       employee_id PK
        string    name
        string    role
        string    email
        string    password_hash
        string    photo_url
        string    nearest_station
        string    created_at
    }
    TABLE {
        int       table_id    PK
        string    label
        int       seats
    }
    VISITOR {
        string    visitor_id      PK
        int       table_id        FK
        string    visit_date
        int       visit_sequence
        int       party
        string    status
        int       staff_called
        int       bill_requested
        string    created_at
    }
    ORDER {
        int       order_id        PK
        string    visitor_id      FK
        string    order_time
        string    note
        int       total_amount
        string    status
        string    created_at
    }
    ORDER_ITEM {
        int       order_item_id   PK
        int       order_id        FK
        int       product_id      FK
        int       quantity
        int       subtotal
        string    status
    }
    BILL {
        int       bill_id         PK
        string    visitor_id      FK
        int       employee_id     FK
        int       subtotal_amount
        int       discount_amount
        int       tax_amount
        int       total_amount
        string    status
        string    payment_method
        string    void_reason
        string    voided_at
        int       voided_by       FK
        string    created_at
    }
    BILL_ITEM {
        int       bill_item_id    PK
        int       bill_id         FK
        string    product_name
        int       unit_price
        float     tax_rate
        int       quantity
        int       subtotal
    }
    DAILY_CLOSE {
        string    date            PK
        string    status
        string    closed_at
        string    memo
    }

    CATEGORY    ||--o{ PRODUCT       : "has"
    PRODUCT     ||--o{ ORDER_ITEM    : "ordered as"
    TABLE       ||--o{ VISITOR       : "seats"
    VISITOR     ||--o{ ORDER         : "places"
    VISITOR     ||--o| BILL          : "settled as"
    ORDER       ||--|{ ORDER_ITEM    : "contains"
    BILL        ||--|{ BILL_ITEM     : "contains"
    EMPLOYEE    ||--o{ BILL          : "handles"
    EMPLOYEE    ||--o{ BILL          : "voids"
          
マスタ系エンティティ — Master Data
CATEGORY マスタ 商品カテゴリ

ドリップ・エスプレッソ・ティー・フード・デザートなど商品分類を管理。スラッグは URL フレンドリーな一意識別子。

属性名制約説明
idintegerPK自動採番 ID
namestringNOT NULLカテゴリ表示名(例: コーヒー)
slugstringUK NOT NULLURL 用識別子(例: coffee)。重複不可
CAT-001CAT-002CAT-005 UC-17UC-18
PRODUCT マスタ 商品

メニュー商品を管理。カテゴリに所属し、POS 画面での注文・売上ランキングの基礎データとなる。価格変更は BILL_ITEM のスナップショットには影響しない。

属性名制約説明
product_idintegerPK自動採番 ID
category_idintegerFK NOT NULL 論理参照CATEGORY.id への論理参照(DB の FK 制約なし)。0 は「未分類」を表す(カテゴリ削除時にリセットされる)
product_namestringNOT NULL商品名(例: カフェラテ)
priceintegerNOT NULL ≥ 0税抜き価格(円)
descriptionstringnullable商品説明文
is_activeintegerNOT NULL有効フラグ(0 / 1)。0 の商品は注文不可
image_urlstringnullable商品画像 URL(R2 オブジェクトキー)
created_atstringNOT NULL登録日時(ISO 8601)
updated_atstringNOT NULL更新日時(ISO 8601)
PROD-001PROD-002PROD-006PROD-007PROD-008 UC-14UC-15UC-16
EMPLOYEE マスタ スタッフ

店舗スタッフを管理。ロール(店長 / バリスタ / ホール)によって操作権限が異なる。会計時の担当者として BILL に紐づく。

属性名制約説明
employee_idintegerPK自動採番 ID
namestringNOT NULLスタッフ氏名
rolestringNOT NULL役割: 店長 / バリスタ / ホール
emailstringUK nullableログイン用メールアドレス(一意)
password_hashstringnullableパスワードハッシュ(PBKDF2-SHA256)。API レスポンスには一切含めない
photo_urlstringnullableプロフィール写真 URL(R2)
nearest_stationstringnullable最寄り駅(勤怠参考情報)
created_atstringNOT NULL登録日(ISO 8601)
EMP-001EMP-002EMP-006EMP-007 UC-19UC-20
TABLE マスタ テーブル席

フロアのテーブル・カウンター席を定義。VISITOR 作成時に紐づき、フロアプランで視覚化される。

属性名制約説明
table_idintegerPK自動採番 ID
labelstringNOT NULL表示名(例: T1、C1)
seatsintegerNOT NULL ≥ 1最大着席人数
TABLE-001TABLE-002 UC-04
トランザクション系エンティティ — Transaction Data
VISITOR トランザクション 来店記録

お客様の 1 来店を表すエンティティ。着席時に生成され、会計完了後は status = checked_out で論理削除される(行は残る)。 visitor_idv_YYYYMMDD_NNN 形式の文字列。 ORDER と BILL の結合軸となる中核エンティティ。

属性名制約説明
visitor_idstringPKv_YYYYMMDD_NNN 形式(例: v_20260526_003)
table_idintegerFK NOT NULLTABLE.table_id への外部キー
visit_datestringNOT NULL来店日(YYYY-MM-DD)
visit_sequenceintegerNOT NULL ≥ 1当日の来店通し番号(NNN)
partyintegerNOT NULL ≥ 1人数
statusstringNOT NULLseated(着席中) / checked_out(退席・会計済み、論理削除)
staff_calledintegerNOT NULLスタッフ呼び出しフラグ(0 / 1)。POS のポーリングで全デバイスに伝播
bill_requestedintegerNOT NULL会計依頼フラグ(0 / 1)。精算完了時に自動クリア
created_atstringNOT NULL着席日時(ISO 8601)
TABLE-003TABLE-004 UC-04UC-05
ORDER トランザクション 注文

POS から送信される 1 回の注文。同一 VISITOR が複数回注文可能(追加注文)。ステータスが served になると調理完了。

属性名制約説明
order_idintegerPK自動採番 ID
visitor_idstringFK NOT NULLVISITOR.visitor_id への外部キー
order_timestringNOT NULL注文時刻(HH:MM)
notestringnullable注文時の備考(例: アイスで)
total_amountintegerNOT NULLORDER_ITEM の subtotal 合計(円)
statusstringNOT NULLpending(受付) / preparing(調理中) / served(提供完了)。この順にのみ遷移可(逆行・スキップ不可)
created_atstringNOT NULL注文受付日時(ISO 8601)
POS-001POS-010POS-011 UC-06UC-07UC-08
ORDER_ITEM トランザクション 注文明細

ORDER に含まれる 1 商品ライン。PRODUCT への参照を持ち、キッチン画面でのステータス管理の単位となる。

属性名制約説明
order_item_idintegerPK自動採番 ID
order_idintegerFK NOT NULLORDER.order_id への外部キー
product_idintegerFK NOT NULLPRODUCT.product_id への外部キー
quantityintegerNOT NULL ≥ 1注文数量
subtotalintegerNOT NULLprice × quantity(円)
statusstringNOT NULLpending(未提供) / served(提供完了)
POS-010KIT-007KIT-008 UC-06UC-09
BILL トランザクション 会計伝票

1 来店の清算を表す。VISITOR と 1 対 0..1 の関係(清算前は存在しない)。 会計確定後も VISITOR・ORDER は論理削除(status 更新)で残るが、商品の価格・名称変更に影響されないよう売上明細は BILL / BILL_ITEM にスナップショットとして永続化される。 消費税は (subtotal - discount) × 0.10 で計算。

属性名制約説明
bill_idintegerPK自動採番 ID(uint32)
visitor_idstringFK 論理参照会計時点の visitor_id を記録。会計履歴の不変性を優先し、DB 外部キー制約は張らない論理参照とする(schema 上も REFERENCES 未指定)
employee_idintegerFK NOT NULL会計担当 EMPLOYEE.employee_id
subtotal_amountintegerNOT NULL値引き前小計(円)
discount_amountintegerNOT NULL値引き額(円・0 以上)
tax_amountintegerNOT NULL消費税額(円・端数は四捨五入)
total_amountintegerNOT NULL税込み請求合計(円)
statusstringNOT NULLsettled(確定) / voided(取消)
payment_methodstringNOT NULLcash / credit
void_reasonstringnullable取消理由(status = voided 時のみ設定)
voided_atstringnullable取消日時(ISO 8601)
voided_byintegerFK nullable取消実行者 EMPLOYEE.employee_id(status = voided 時のみ設定)
created_atstringNOT NULL会計確定日時(ISO 8601)
BILL-001BILL-008BILL-009BILL-012BILL-013 UC-10UC-11UC-12UC-13
BILL_ITEM トランザクション 会計明細(スナップショット)

BILL に含まれる商品明細。product_name・unit_price はスナップショットとして保持するため、 後から PRODUCT の価格を変更しても過去の会計履歴には影響しない。 ORDER_ITEM を集約(同一商品は合算)して生成される。

属性名制約説明
bill_item_idintegerPK自動採番 ID
bill_idintegerFK NOT NULLBILL.bill_id への外部キー
product_namestringNOT NULLスナップショット: 会計時点の商品名
unit_priceintegerNOT NULLスナップショット: 会計時点の単価(円)
tax_ratefloatNOT NULL適用税率(例: 0.10)
quantityintegerNOT NULL ≥ 1合算数量
subtotalintegerNOT NULLunit_price × quantity(円)
BILL-005BILL-006BILL-007 UC-10UC-11
締め系エンティティ — Daily Operation Data
DAILY_CLOSE 運用 日次締め記録

1 営業日の締め状態を管理する運用テーブル。 日付を主キーとし、締め処理の実行で status = closed に更新される。 売上金額自体は保持せず(BILL から都度集計)、締め完了フラグと特記メモのみを記録する。 締め実行者(API レスポンスの closed_by)は JWT から取得して返却するのみで、このテーブルには永続化しない。

属性名制約説明
datestringPK対象営業日(YYYY-MM-DD)
statusstringNOT NULLopen(未締め) / closed(締め済み)
closed_atstringnullable締め実行日時(ISO 8601)
memostringnullable特記事項(機材トラブル・廃棄ロス・引継ぎ事項など)
CLOSE-008CLOSE-009CLOSE-010
エンティティ A カーディナリティ エンティティ B 説明
CATEGORY 1 ─── 0..* PRODUCT 1 カテゴリに 0 件以上の商品が属する
PRODUCT 1 ─── 0..* ORDER_ITEM 1 商品が複数の注文明細で参照される(ライブ参照)
TABLE 1 ─── 0..* VISITOR 1 テーブルに同時に 0..1 来店、通算では複数来店
VISITOR 1 ─── 0..* ORDER 1 来店で 0 件以上の注文(追加注文あり)
VISITOR 1 ─── 0..1 BILL 1 来店に最大 1 件の会計(清算後に VISITOR は論理削除 = checked_out)
ORDER 1 ─── 1..* ORDER_ITEM 1 注文に 1 件以上の明細(空注文は禁止)
BILL 1 ─── 1..* BILL_ITEM 1 会計に 1 件以上の明細(空会計は禁止)
EMPLOYEE 1 ─── 0..* BILL 1 スタッフが複数の会計を担当する(employee_id
EMPLOYEE 1 ─── 0..* BILL 1 スタッフが複数の会計を取消す(voided_by・取消時のみ)
1

BILL_ITEM はなぜ product_name・unit_price をスナップショット保持するのか

商品の価格や名称は将来変更される可能性がある。BILL_ITEM が PRODUCT への外部キーのみを保持する設計では、 価格変更後に過去の会計伝票を参照すると誤った金額が表示される。 そのため会計確定時点の product_name・unit_price・tax_rate を BILL_ITEM に複製(スナップショット) することで、 会計履歴の不変性を保証する。これは EC サイトの注文履歴と同様のパターン。

2

BILL_ITEM に category_id がない理由と日次締めでの対応

BILL_ITEM はスナップショット設計のため、あえて category_id を持たない(カテゴリ変更も不変性を崩さないため)。 日次締めのカテゴリ別集計では BILL_ITEM.product_namePRODUCT.product_name の名前逆引きで現在のカテゴリを推定する。 本番では BILL_ITEM に category_id / category_name をスナップショットとして追加し、名前逆引きに依存しない集計に切り替える設計を推奨する。

3

DAILY_CLOSE を独立テーブルにした理由

売上金額・件数・カテゴリ別内訳は BILL / BILL_ITEM から都度集計 するため、 DAILY_CLOSE に金額そのものは保持しない(二重管理を避け、取消反映漏れを防ぐ)。 DAILY_CLOSE が持つのは「その日を締めたか(status)」「締め時刻」「特記メモ」のみ。 日付を主キーにすることで ON CONFLICT(date) DO UPDATE による冪等な締め処理が可能。 売上集計の事前計算(マテビュー化)は将来データ量が増えた段階での最適化余地として残す。

4

VISITOR は会計後に論理削除(status = checked_out)

会計確定時、VISITOR を物理削除せず UPDATE visitors SET status = 'checked_out' で論理削除し、関連 ORDER は served に更新する。 フロアプランは status = 'seated' のみを「使用中」として描画するため、 論理削除だけで自動的に「空席」に戻る。 行を残すことで BILL.visitor_id から来店記録(テーブル・人数・来店時刻)を遡って参照でき、会計履歴と来店情報の突合が可能。 当日の来店通し番号(visit_sequence)も checked_out を含めて採番するため番号の連続性が保たれる。

5

visitor_id の採番規則(v_YYYYMMDD_NNN)

v_20260526_003 のように日付 + 当日通し番号とする。 これにより human-readable なうえ、日付フィルタリングを文字列前方一致で高速化 できる。 UUID を使わない理由は、POS スタッフが口頭確認する際「本日 3 番のお客様」と伝えやすいため。 来店数が 999 件を超える場合は 4 桁(0001〜9999)への拡張を推奨。

6

payment_method を BILL に直接保持する単純化

一般的には「支払い方法マスタ」テーブルを用意するが、本システムでは現金・クレジットの 2 種に限定されるため、 BILL.payment_method に文字列 enum(cash / credit)として直接保持する。 電子マネー・QR コード決済等を追加する際は PAYMENT_METHOD テーブルへの正規化を推奨。

7

金額はすべて整数(円)で保持する

price / subtotal / subtotal_amount / discount_amount / tax_amount / total_amount など金額系カラムはすべて integer 型(円単位・小数なし) で保持する。 日本円は最小単位が 1 円のため小数が不要で、浮動小数を使うと税計算で丸め誤差が蓄積する。 唯一 BILL_ITEM.tax_rate のみ float(0.10)だが、これは「率」であり金額ではない。 消費税は Math.round((subtotal - discount) × 0.10) で計算し、結果を整数に丸めて保存する。

8

合計金額・税額・担当者はサーバー側で確定する(クライアント値を信頼しない)

会計確定時、subtotal_amount は 対象 VISITOR の未会計 ORDER を DB から再集計して算出し、 クライアントが送信した合計値は一切採用しない。 税額・合計も同じくサーバー側で再計算する。 会計担当の employee_id もリクエストボディではなく JWT ペイロードから取得し、他スタッフへのなりすまし計上を防ぐ。 これにより、改ざんされたリクエストが来ても DB 上の金額・担当者の整合性が保たれる(信頼境界をサーバーに置く設計)。