---
title: "価格承認の監査ログを設計する：誰が、どのルールで、何を承認したか"
description: "Shopifyのログには承認ルールもバージョンも残りません。見積の承認フローに必要な記録の中身、追記のみで書く方法、ハッシュチェーンが証明できることを解説します。"
url: "https://www.quotway.com/ja/blog/pricing-approval-audit-log"
type: "blog post"
category: "Engineering"
published: "2026-10-14"
verified: "2026-10-14"
shopify_api_version: "2026-10"
audience: "Shopifyの見積レイヤーで値引きと価格の承認を開発・レビューする開発者とエージェンシー"
scope: "API 2026-10時点で、Shopifyの横に置く価格承認の監査記録。Shopifyのアクティビティログ、タイムライン、Eventオブジェクトが保持しないもの、項目ごとの記録（対象バージョンと見積全体の値引きの係数、ルールのリビジョンと入力値、行為者、時刻、スキップやオーバーライドを含む結果）、状態を条件とする1つのトランザクションでの決定の書き込み、データベースで強制する追記のみ、検証とアンカーを伴うハッシュチェーンが証明するもの、監査ログとアウトボックスとアクティビティフィードの分離、イベントソーシング、保存期間と個人データ"
locale: "ja"
source: "QuotWay - B2B Quote & Negotiation App for Shopify"
---

# 価格承認の監査ログを設計する：誰が、どのルールで、何を承認したか

承認が証拠になるのは、何か月も後になって、誰が決めたのか、正確にどの数字に対してか、その時点でのどのルールに基づいてか、そしてスキップされたものも含めて各ステップに何が起きたのかを示せる場合だけです。Shopifyのログはその記録を持っていません。アクティビティログ、タイムライン、APIのイベントには、ルールもしきい値もバージョンも含まれません。そのため、値引きを承認に回す見積レイヤーは、見積の承認フローのための監査ログを自分で持つ必要があります。設計上の問いは具体的です。記録に何を含めるか、どう書き込むか、どうやって追記のみ（append-only）に保つか、そしてハッシュチェーンが何を証明し、何を証明しないかです。

このページでは、その設計を項目ごとに、簡略化したコードとともに説明します。承認ポリシーの作り方は[承認ワークフロー](/blog/shopify-b2b-approval-workflows)で、何を必須にすべきかは[承認マトリクス](/blog/b2b-quote-approval-matrix)で扱っています。このページが扱うのは、それらの決定が後に残す記録です。QuotWayを例にする箇所では、現在の動作をそのまま説明し、まだ実装していない部分ははっきりそう書きます。そうした部分は推奨であり、推奨と明記しています。

以下のShopifyに関する事実は、2026年10月14日にAPIバージョン2026-10でShopify自身のページと照合しました。出典は末尾にあります。

## 見積の承認フローで、監査ログが答えるべき問い
「この値引きを誰が、どのルールで承認したのか」という問いは5つの問いに分かれ、記録にはそれぞれに対応する部分が必要です。

1. **何に対して？** 正確なプロポーザルのバージョンとその数字。見積全体の値引きが各行に与える影響も含みます。
2. **どのルールで？** 一致したポリシー、一致した分岐、一致の根拠となった入力値、そして*その時点での*ポリシー。
3. **誰が？** 決定した人またはシステム、そのロール、誰の代理か、ほかに誰が決定できたか。
4. **いつ、どの順序で？** サーバー時刻と、決定がコミットされた順序。
5. **どんな結果で？** 承認、却下。そして見落とされる結果、つまりスキップされたステップとその理由、根拠を伴うオーバーライド、新しいバージョンによって置き換えられたチェーン、タイムアウトです。

## Shopifyが記録するもの、しないもの
Shopifyには3つのログがありますが、どれも承認の記録ではありません。

- **ストアのアクティビティログ**は、"recent actions taken by store owners and users"（ストアのオーナーとユーザーが最近行った操作）を表示します。商品の削除、設定の変更、アプリへのアクセス付与などです。閲覧専用で、"can't be exported or downloaded"（エクスポートもダウンロードもできない）とされ、"displays a maximum of 250 results"（最大250件まで表示）です。
- 各注文、下書き注文、顧客、在庫移動の**タイムライン**。加えて、ディスカウントのページにあるそのディスカウント自身のアクティビティログ。
- **ユーザー管理のアクティビティログ**。対象となるユーザーについて、"when a user or user role is created, edited or deleted"（ユーザーまたはユーザーロールが作成、編集、削除されたとき）を記録します。

Admin APIでは、下書き注文の `events` が `Event` オブジェクトを返します。中身はアクション、メッセージ、作成者、ユーザーとアプリのどちらが起こしたか、タイムスタンプです。ポリシー、しきい値、根拠、決定の対象となったバージョンを持つフィールドはありません。また、API 2026-10でコンテキスト価格に加わった `auditTrail` は、*カタログ価格*がどう決まったかを説明するもので、これも承認の記録ではありません。どのShopifyスタッフが操作したかを知るには制限付きのアクセスも必要です（スコープの規則は[自作か購入か](/blog/build-vs-buy-custom-shopify-rfq-app#gates)を参照）。そのため、見積レイヤーは承認者の識別情報を自分で保持します。

## 記録を項目ごとに
| 部分 | 項目 | 後で答える問い | ないと何が起きるか |
| --- | --- | --- | --- |
| **対象** | 見積、プロポーザルのバージョンIDと番号。そのバージョンの合計、見積全体の値引きとその係数。各行の実効価格 | 「正確には何が承認されたのか」 | 後のバージョンが、承認されたバージョンと取り違えられる |
| **ルール** | ポリシーIDと**リビジョン**。一致した分岐。計算した入力値（金額と通貨、全体の値引き率と行の最大値引き率、デポジット%、顧客タグ） | 「どのしきい値でこれが通ったのか」 | ポリシーを編集すると、過去が説明できなくなる |
| **行為者** | 行為者のIDと種別（スタッフ、バイヤー、システム）。チェーン開始時に確定した承認者の集合。委任の元と先。外部承認者のメールアドレス。その時点でのロール | 「この人に決定する権限はあったのか」 | ロールの変更が、誰が承認できたかを書き換えてしまう |
| **時刻** | サーバー時刻。コミットの順序 | 「何が先に起きたのか」 | 同時に行われた決定の順序がつけられない |
| **結果** | 承認、却下、理由付きのスキップ、根拠付きのオーバーライド、置き換え、キャンセル、タイムアウト | 「なぜステップ2は実行されなかったのか」 | スキップされたステップが、欠落したステップと見分けられない |

### 何について決定したか：見積全体の値引きの落とし穴
記録が指す先は、見積ではなく、変わることのないプロポーザルのバージョンにします。送信済みのプロポーザルは変わらず、変更は新しいバージョンになり、古いバージョンでまだ進行中の承認チェーンはそれによって置き換えられます（[プロポーザルとバージョン](/docs/negotiation/proposals-and-versions)）。ただし、バージョンIDだけでは足りません。ルールが見た数字は*計算された*ものだからです。

その理由を示すのが次のケースです。[バイヤーの合計を行から足し合わせない](/blog/never-sum-the-total-from-the-rows)のと同じ理由です。2026年9月30日まで、QuotWayは値引きの条件を行の価格だけで判定していました。そのため、すべての行を定価にして見積全体に大きな値引きをかけたプロポーザルは、値引きの承認を発生させませんでした。それ以降は、各行をバイヤーが実際に支払う額、つまり見積全体の値引きの按分を差し引いた後の価格で判定しています。

```ts
// Factor applied to every line: the buyer pays (subtotal - orderDiscount) / subtotal of each price.
function orderDiscountFactor(subtotal: Decimal, orderDiscount: Decimal): Decimal {
  if (subtotal.lte(0) || orderDiscount.lte(0)) return new Decimal(1);
  if (orderDiscount.gte(subtotal)) return new Decimal(0);
  return subtotal.minus(orderDiscount).div(subtotal);
}

function approvalInputs(version: VersionSnapshot) {
  const factor = orderDiscountFactor(version.subtotal, version.orderDiscount);
  let catalogTotal = new Decimal(0), paidTotal = new Decimal(0);
  let deepestLinePct: number | undefined;
  for (const line of version.lines) {
    const effective = line.finalPrice.times(factor);          // what the buyer actually pays
    catalogTotal = catalogTotal.plus(line.catalogPrice.times(line.quantity));
    paidTotal = paidTotal.plus(line.finalPrice.times(line.quantity));
    const base = line.baselinePrice ?? line.catalogPrice;
    if (base.gt(0)) {
      const pct = base.minus(effective).div(base).times(100).toNumber();
      deepestLinePct = Math.max(deepestLinePct ?? -Infinity, pct);
    }
  }
  const blendedPct = catalogTotal.isZero()
    ? undefined
    : catalogTotal.minus(paidTotal.times(factor)).div(catalogTotal).times(100).toNumber();
  return { factor, blendedPct, deepestLinePct };
}
```

行の価格だけを保存した記録では、チェーンが「本来は動かないはずなのに」動いたように見えます。変更前であれば、25%の注文値引きが承認なしに、目に見える理由もなく通ったように見えます。計算した入力値、つまり係数と、全体の値引き率および行の最大値引き率を、バージョンIDの隣にチェーン上に保存してください。これは推奨です。QuotWayが現在保存しているのはバージョンと短い理由であり、入力値は保存していません。

### どのルールが、どのバージョンで一致したか
ポリシーをその場で編集すると、「どのしきい値で？」への答えが消えます。対策はリビジョンです。編集のたびにリビジョンを上げ、古いリビジョンを残し、チェーンの開始時にそのリビジョン、または一致したルールのスナップショットをチェーンに書き込みます。以下は推奨する形で、QuotWayの現在のスキーマではありません。

```sql
-- On the approval chain, written once when it starts:
policy_id         text   not null,
policy_revision   int    not null,   -- bumped on every policy edit; old revisions kept
matched_rule      jsonb  not null,   -- the scope and conditions as they were
inputs            jsonb  not null,   -- amount, currency, rate, blended %, deepest line %,
                                     -- order-discount factor, deposit %, customer tags
step_skips        jsonb  not null    -- [{ step, reason: "rule did not match" | "missing: customerTags" }]
```

現在のQuotWayはポリシーをその場で編集し、ポリシーIDと、金額ルールでは金額と通貨を示す短い理由を保存しています。リビジョン、入力値、各スキップの理由を記録することが、QuotWayの次のステップです。

### 誰が決定したか
承認者はチェーンの開始時に具体的な人に確定させます。そうすれば、後でロールが変わっても、誰が決定できたかは変わりません。QuotWayはこれを行っています。すべての決定を、行為者のIDと種別とともに記録します。種別は、スタッフ、バイヤー側の承認者（外部承認者の場合は、その人が操作に使ったメールアドレス）、またはタイムアウトによる自動決定を表す `system` です。委任は、誰から誰へ移ったかを持つ独立した行として記録します。そして、決定時点での承認者のロールを保存します。スタッフの記録は変わるからです（[スタッフとロール](/docs/staff/staff-and-roles)）。

### 結果：見落とされるものも含めて
承認と却下は簡単です。記録にはそれに加えて、条件が一致しなかったためにスキップされたステップとその理由、オーバーライドとそれを行った人およびその根拠、新しいバージョンの送信によって置き換えられたチェーン、キャンセルされたチェーン、そしてタイムアウトによる決定（行為者はシステム、理由付き）が必要です。理由のないスキップは、実行されなかったステップと見分けがつきません。

## 書き込み：状態を条件とする1つのトランザクション
2人の承認者が同時に「承認」を押すことがあります。各決定を新しい行として書き込み、ステップがまだアクティブな場合に限ってステップを進めます。こうすると、ちょうど1つの決定だけが反映され、もう一方は競合になります。チェーンの次のステップと終端イベントも同じトランザクションで処理します。QuotWayが決定を書き込む方法を簡略化すると、次のとおりです。

```ts
await db.$transaction(async (tx) => {
  const step = await tx.approvalSteps.findUnique({ where: { id: stepId } });
  if (step.state !== "ACTIVE") throw new StepNotActive();
  if (actor.type !== "SYSTEM" && !step.approvers.includes(actor.id)
      && !(actor.email && step.approvers.includes(actor.email))
      && step.delegateId !== actor.id) throw new NotAnApprover();

  // 1. The decision is a new row, never an update.
  await tx.approvalDecisions.create({ data: {
    chainId: step.chainId, stepId, decision, rationale,
    actorId: actor.id, actorType: actor.type, actorEmail: actor.email,
  }});

  // 2. Move the step only if nobody beat us to it - 0 rows means a concurrent decision.
  await tx.approvalSteps.update({
    where: { id: stepId, state: "ACTIVE" },
    data: { state: next(decision), resolvedAt: new Date() },
  });

  // 3. Advance the chain; on a final verdict, append the quote event - same transaction.
  const outcome = advance(await tx.approvalSteps.findMany({ where: { chainId: step.chainId } }));
  if (outcome.terminal) await appendEvent(tx, { quoteId, type: verdictEvent(outcome), chainId: step.chainId });
});
// After commit: notify the next approvers, send the released proposal, dispatch webhooks.
```

メール、リリースされたプロポーザル、Webhookといった副作用は、コミットの後に、同じトランザクションで書き込んだアウトボックスを通じて実行します。このパターンは[冪等な変換の記事](/blog/idempotent-draft-order-creation#record-result)で扱っています。

## 追記のみは、慣習ではなくデータベースの性質にする
「アプリは決定を更新しない」というのは慣習です。これを性質にします。

- アプリケーションが接続するロールから、監査テーブルに対する `UPDATE` と `DELETE` を**取り消します（REVOKE）**。PostgreSQLでは、テーブルの所有者は "is always treated as holding all grant options"（常にすべての権限付与オプションを持つものとして扱われる）ため、アプリは所有者として接続してはいけません。あるいは、更新や削除で例外を発生させるトリガーを追加します。
- **監査テーブルへのカスケードを置かない。** ポリシーやステップから決定への `ON DELETE CASCADE` 付き外部キーは、アプリケーションが書いたことのない削除です。ポリシーを削除したり、ポリシーからステップを外したりしても、過去の決定が一緒に消えてはいけません。ポリシーは削除せずにアーカイブします。
- **訂正は新しい行で行う。** 誤った決定には、編集ではなく、それを打ち消す決定で応えます。

QuotWayは決定を新しい行として書き込み、編集はしません。しかし現時点では、決定の記録が、それが属するポリシーやステップと一緒に削除されることがありえます。その削除を止めることが必要な修正であり、それまでは、QuotWayは履歴を追記のみとは呼びません。

## ハッシュチェーン：証明すること、しないこと
ハッシュチェーンでは、各行に直前の行のハッシュを保存します。そのため、1行を編集または削除すると、それ以降のすべてのハッシュが合わなくなります。QuotWayは見積のイベントをこの方法で書き込み、連携用のアウトボックスの行も同じトランザクションで書き込みます。

```ts
async function appendEvent(tx: Tx, e: EventInput) {
  const prev = await tx.auditEvents.findFirst({
    where: { quoteId: e.quoteId }, orderBy: { createdAt: "desc" }, select: { hash: true },
  });
  const createdAt = new Date();
  const hash = sha256((prev?.hash ?? "") + "|" + canonicalJson({ ...e, createdAt: createdAt.toISOString() }));
  const row = await tx.auditEvents.create({ data: { ...e, createdAt, prevHash: prev?.hash ?? null, hash } });
  await tx.outbox.create({ data: toPublicEvent(row) });   // commits or rolls back with the event
  return row;
}
// canonicalJson = JSON with object keys sorted recursively, so the hash is reproducible.
```

これが何を証明するかは、さらに3つの条件に左右され、どれもただでは手に入りません。誰かがチェーンを**検証**する、つまりすべてのハッシュを順に計算し直す必要があります。最新のハッシュを、書き込む側が書き換えられない場所に**アンカー**する必要があります。テーブル全体を書き換えられる者は、チェーン全体を計算し直せるからです。NISTのログ管理ガイドは、ダイジェストを安全に保管すること、たとえば "on read-only media"（読み取り専用の媒体に）保管することを推奨しています。そして、追記を**直列化**する必要があります。そうしないと、2つの書き込み元がチェーンを分岐させかねません。以下は推奨する形で、QuotWayのコードではありません。

```ts
// Serialise appends per quote so two writers can't fork the chain:
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtext(${quoteId}))`;

// Verify: recompute every hash in order; the first mismatch is where the record changed.
function verify(rows: AuditRow[]): { ok: true } | { ok: false; at: string } {
  let prev = "";
  for (const r of rows) {                 // ordered by a per-quote sequence, not by timestamp
    if ((r.prevHash ?? "") !== prev) return { ok: false, at: r.id };
    if (sha256(prev + "|" + canonicalJson(fieldsOf(r))) !== r.hash) return { ok: false, at: r.id };
    prev = r.hash;
  }
  return { ok: true };
}
// Anchor: copy the latest hash per quote to storage the app can't rewrite (a write-once bucket).
```

QuotWayのイベントは直前のイベントのハッシュを持っています。チェーンの検証とアンカーはまだ作っていないため、QuotWayはこの記録を改ざん防止（tamper-proof）とは呼びません。OWASPのログ記録ガイダンスも同じことを一文で述べています。"Build in tamper detection so you know if a record has been modified or deleted"（記録が変更または削除されたことが分かるよう、改ざんの検出を組み込むこと）。検出には検証の仕組みが必要です。

もう1つ、範囲の狭い封印を持っておく価値があります。QuotWayは、承諾された行に対する**承諾ハッシュ**（バージョンIDと、各行のID、数量、単価、合計から計算）を保存し、変換画面、Shopifyの注文カード、確認用PDFに表示しています。これにより、注文をバイヤーが承諾した内容そのものまでたどることができます。

## 監査ログと操作ログの違い：3つのログ、3つの役割
| | 監査ログ | 連携用アウトボックス | アクティビティフィード |
| --- | --- | --- | --- |
| 読み手 | 監査人、紛争、1年後の自分 | 他のシステム（ERP、CRM、Webhook） | ダッシュボードを見る人 |
| 中身 | すべての決定を完全に | 配信するための結果 | 選別したもの |
| 個人データ | 決定に必要なものだけ | なし | 表示名 |
| 保存期間 | それが承認した見積と同じ期間 | 配信されるまで、その後は短期間 | 短期間 |
| 変更可能性 | 追記のみ | 1回書き込み、配信し、期限切れにする | 射影（プロジェクション）。自由に作り直せる |

日本語で「操作ログ」と呼ばれるものに近いのは、この表のアクティビティフィードです。人が画面で読むための、選別された分かりやすい一覧です。監査ログは、それとは別に、証拠として残す完全な記録です。両者は読み手も、中身も、保存期間も異なります。

OWASPは、監査やトランザクションの証跡を他のログと分けて保つよう勧めています。その理由がこの表です。1つの保存先で、保存期間の異なる3種類の読み手に応えることはできません。[見積のアーキテクチャ](/blog/shopify-b2b-quote-architecture)が監査証跡を見積レイヤーに置くのも同じ理由です。QuotWayでは、アウトボックスが運ぶのは承認の*結果*だけで個人データは含まず、ダッシュボードのアクティビティフィードは承認イベントを含まない選別された一覧です。

## イベントソーシングか、監査ログか
イベントソーシングは、"the full series of actions taken on an object in an append-only store"（あるオブジェクトに対して行われた一連の操作のすべてを、追記のみのストアに）保存し、そこから状態を組み立て直します。Microsoftのガイダンスはこれを "a complex pattern that introduces significant trade-offs"（大きなトレードオフを伴う複雑なパターン）と呼び、"for most systems... traditional data management is sufficient"（ほとんどのシステムでは従来のデータ管理で十分）と述べています。Martin Fowlerの元の解説は、監査ログはその副産物の1つであり、"could also do... with more regular logging mechanisms"（もっと普通のログの仕組みでも実現できる）と書いています。「誰が、どのルールでこれを承認したか」に答えるには、普通のテーブルの隣に置いた追記のみの決定テーブルで十分です。Shopifyの横に置く見積レイヤーは、完全な再生にも向きません。イベントを再生すればShopifyにもう一度触れることになり、外部システムは "don't know the difference between real processing and replays"（実際の処理と再生の違いが分からない）からです。

## 保存期間と個人データ
最小化は書き込みの時点で行います。追記のみの行は後から掃除できません。OWASPは、セッション識別子、アクセストークン、機微な個人データを、ログに残すべきでないものとして挙げています。NISTは "avoid recording unneeded sensitive data"（不要な機微データを記録しない）よう述べています。人はIDで保持し、連絡先の詳細は別の場所に置きます。Microsoftのガイダンスは、それか、破棄できる本人ごとの鍵で個人データを暗号化することを、追記のみのストアでの消去請求への答えとして挙げています。決定は、それが承認した見積と同じ期間保持します。期間は自社の義務によって決まるもので、私たちは具体的な期間を挙げません。ログと個人データについてベンダーに確認すべき質問は、[セキュリティレビューのチェックリスト](/blog/shopify-b2b-security-review-checklist)にあります。

## 何か月も後に問いに答える
「見積QW-1042のバージョン3にある18%の値引きを、誰が、どのルールで承認したのか」。上の記録があれば、次のように答えられます。

1. バージョン3のチェーンを見つけます。見積からではありません。見積には後のバージョンがあるかもしれません。
2. チェーンのルールを読みます。ポリシーとそのリビジョン、そして一致した入力値です。見積全体の値引き係数0.95を含めた全体の値引き率18.4%が、15%のしきい値に対して判定されています。
3. 決定をコミット順に読みます。ステップ1は10:02に営業マネージャーが承認。ステップ2は条件（デポジット20%未満）が一致しなかったためスキップ。ステップ3は、委任を受けた経理の承認者が承認。
4. 注文の承諾ハッシュが、バージョン3の承諾された行と一致することを確認します。

ここで1つだけ変えてみます。先月、ポリシーのしきい値が20%に引き上げられたとします。チェーンにリビジョンがなければ、ステップ2は「今なら発動しない」と読め、なぜ当時は動いたのか誰にも説明できません。リビジョンがあれば、答えは1行で済みます。

## 自分で作るときのチェックリスト
- すべての決定は新しい行であり、何も決定を更新・削除しない。
- アプリのデータベースロールは監査テーブルに `UPDATE` も `DELETE` もできず、監査テーブルへのカスケードもない。
- 各チェーンは、見積ではなく、変わることのないプロポーザルのバージョンを指す。
- 各チェーンは、ポリシーのリビジョン（またはルールのスナップショット）と、見積全体の値引き係数を含む計算済みの入力値を保存する。
- 承認者はチェーンの開始時に確定し、決定時点のロールを保存する。
- 委任は元と先を持つ行であり、外部承認者は操作に使った識別情報で記録する。
- スキップは理由を、オーバーライドは人と根拠を持ち、タイムアウトはシステムとして記録する。
- 2つの決定が同時に行われても、反映されるのはちょうど1つ。
- 副作用は、同じトランザクションで書き込んだアウトボックスを通じて、コミット後に実行する。
- ハッシュチェーンを使うなら、追記を直列化し、検証の仕組みを動かし、最新のハッシュを書き込む側の手が届かない場所にアンカーする。
- 監査ログ、アウトボックス、アクティビティフィードは、保存期間の異なる別々の保存先にする。
- トークン、セッションID、決定に不要な個人データは含めない。

自作ではなくアプリを評価する場合は？ 同じリストがそのまま質問として使えます。[評価チェックリスト](/blog/shopify-b2b-app-evaluation-checklist)に監査ログのテストがあり、この部分を作るコストは[自作か購入か](/blog/build-vs-buy-custom-shopify-rfq-app#components)で見積もっています。

## QuotWayでの位置づけ
マーチャントの承認ワークフローとバイヤー側の承認チェーンはProfessional以上のプランで利用でき、見積に適用されます。並列・条件付きのステップ、タイムアウト、エスカレーション、委任はEnterpriseです（[承認ワークフロー](/docs/approvals/approval-workflows)、[プラン](/pricing)）。チェーンは1つのプロポーザルのバージョンに属し、新しいバージョンがそれを置き換えます。承認者はチェーンの開始時に確定し、すべての決定は行為者、その種別、根拠とともに新しい行として書き込まれ、同時に行われた2つの決定のうち反映されるのは1つです。見積のイベントは直前のイベントのハッシュを持ち、承諾ハッシュはShopifyの注文カードに表示されます。イベントと承諾ハッシュを含む見積の記録は、デフォルトで7年間保持されます。承認が何をブロックするかは[承認の適用のしくみ](/docs/approvals/how-approval-enforcement-works)で説明しています。2026年9月30日以降、値引きの条件は、各行を見積全体の値引きの按分を差し引いた後の価格で判定します。Enterprise APIの承認Webhookが運ぶのはチェーンの結果だけで、個人データは含みません（[承認機能](/features/approvals)）。

## よくある質問

### 価格承認の監査ログには何を記録すべきですか？

プロポーザルのバージョンとその数字、ルールとそのリビジョンおよび一致の根拠となった入力値、誰が誰の代理として決定したか、いつか、そして結果です。結果には、理由付きのスキップと根拠付きのオーバーライドも含めます。

### Shopifyには値引き承認の監査ログがありますか？

ありません。Shopifyにあるのは、ストアのアクティビティログ、注文と下書き注文のタイムライン、ディスカウント自身のアクティビティログ、ユーザー管理のログです。どれも、ルール、しきい値、決定の対象となったバージョンを保存していません。

### 監査ログは変更不可にすべきですか？

追記のみにします。訂正は新しい行で行います。これをデータベースで強制してください。アプリケーションのロールから更新と削除の権限を取り消すか、トリガーを使い、監査テーブルにはカスケード削除を及ぼさないようにします。

### ハッシュチェーンだけで監査ログを改ざん防止にできますか？

それだけではできません。改ざんが分かるのは、誰かがチェーンを検証し、最新のハッシュが書き込む側には変更できない場所に保存されている場合だけです。そうでなければ、テーブルを書き換えられる者はすべてのハッシュを計算し直せます。

### イベントソーシングと監査ログ、どちらを選ぶべきですか？

履歴から状態を組み立て直す必要がない限り、監査ログです。イベントソーシングは大きなトレードオフを伴い、イベントの再生はShopifyのような外部システムとうまく両立しません。

### 見積全体の値引きは値引き承認の対象に含めるべきですか？

含めるべきです。各行を、見積全体の値引きの按分を差し引いた後の価格で判定し、使った係数を保存します。QuotWayは2026年9月30日からこの方法で判定しています。

### 後で承認ポリシーが変わったら、記録はどうなりますか？

チェーンがポリシーIDしか保存していなければ、どのしきい値が適用されたのか分からなくなります。チェーンの開始時に、ポリシーのリビジョンか、一致したルールのスナップショットを保存してください。

### 承認の記録はどのくらいの期間保存すべきですか？

それが承認した見積と同じ期間です。期間は自社の義務によって決まります。私たちは法定の期間を挙げません。

### 監査ログとアクティビティフィード（操作ログ）の違いは何ですか？

アクティビティフィードは、ダッシュボード向けに選別した、読みやすい要約です。監査ログは、証拠として残す完全な記録です。読み手、中身、保存期間が異なるため、別々の保存先に分けます。

## 出典

2026年10月14日に確認：

- Shopify：[アクティビティログ](https://help.shopify.com/en/manual/shopify-admin/activity-logs)、[ユーザー管理のアクティビティログ](https://help.shopify.com/en/manual/your-account/users/security/user-management-activity-log)、[Event（2026-10）](https://shopify.dev/docs/api/admin-graphql/2026-10/interfaces/Event)、[BasicEvent（2026-10）](https://shopify.dev/docs/api/admin-graphql/2026-10/objects/BasicEvent)、[DraftOrder（2026-10）](https://shopify.dev/docs/api/admin-graphql/2026-10/objects/DraftOrder)
- [OWASP Logging Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html)：イベントの属性、除外すべきデータ、改ざんの検出、監査証跡を分けて保つこと、保存期間
- NIST、[SP 800-92, Guide to Computer Security Log Management](https://csrc.nist.gov/pubs/sp/800/92/final)：保管したダイジェストによる完全性の確認、不要な機微データを避けること（[改訂版](https://csrc.nist.gov/pubs/sp/800/92/r1/ipd)はドラフトとして存在）
- Microsoft Learn、[Event Sourcing pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing)：トレードオフ、従来のデータ管理で十分な場合、追記のみのストアにおける個人データ
- Martin Fowler、[Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html)：定義、別の手段による監査ログ、再生と外部システム
- PostgreSQL、[REVOKE](https://www.postgresql.org/docs/current/sql-revoke.html)：更新と削除の権限の取り消し、所有者がすべての権限を保持し続けること

QuotWayの動作は、2026年10月14日に確認したアプリのソースに基づいて説明しています。コードは簡略化し、内部の名前を取り除いています。
