GetFeedback と LeaveFeedback でフィードバックデータ取得と自動評価送信を実装する

前回の記事はこちら

【連載#17】eBay Trading API:GetFeedback と LeaveFeedback でフィードバックデータ取得と自動評価送信を実装する

はじめに

本記事は、全42回にわたる「eBay API 実践ガイド」の第17回です。

前回(#16)は、GetCategories と GetCategoryFeatures を用いて eBay のカテゴリ構造とバリデーションルールをプログラムから取得する方法を解説しました。これにより、出品する前の「カテゴリ選定・入力チェック」フェーズが自動化でき、出品システムとしての精度が大きく高まりました。

今回のテーマは 「フィードバック(評価)管理」 です。eBay においてフィードバックは、セラーのアカウント健康度と検索ランキングに直結する極めて重要な指標でありながら、多くのセラーが手動対応のまま放置しています。本記事では、GetFeedback と LeaveFeedback という2つの Trading API メソッドを活用し、評価の取得・分析から発送完了後の自動ポジティブ返信まで、Python で完結する運用システムを構築します。

この記事で得られること:

  • GetFeedback を使ったセラー評価一覧の全件取得と、Positive / Neutral / Negative 別の集計・スコア分析の実装方法。
  • 評価自動化で必ず踏む「タイミング問題」「二重送信」「ポリシー制約」という3大落とし穴とその回避策。
  • 発送完了を検知してから自動的に Positive フィードバックを送る、本番稼働レベルの Python スクリプト。

背景・なぜこれが重要か (Motivation)

「好評率が高ければ万事大吉? フィードバックなんて自然に集まるものでは?」

これは eBay 初心者が陥りがちな認識です。実際には、フィードバックは意識的に管理しなければ確実に劣化していく指標です。

eBay において Top Rated Seller(TRS)の資格を得るには、直近12ヶ月で Positive Feedback Percentage が 98% 以上、かつ一定件数以上の評価が必要です。TRS であることは単なるステータスシンボルではありません。検索結果の Best Match アルゴリズムにおいてポジティブな重み付けが行われるため、商品の可視性(Visibility)が向上し、実質的に「広告費ゼロの SEO 効果」をもたらします。

逆に、Negative(ネガティブ)評価が数件積み重なると、Positive Percentage が急落し、TRS 資格を失うのは想像以上に早いです。例えば、1,000件の評価のうち20件が Negative になると Positive Percentage は 98.0% を下回り、TRS ラインを割り込みます。一度失った TRS 資格を回復するには数ヶ月単位の時間がかかります。

一方、バイヤー目線では、購入後にセラーへのフィードバックを送ることを忘れているケースが多くあります。セラー側から LeaveFeedback で先に Positive を送ることで、バイヤーが「そういえば評価しなきゃ」と思い出し、返礼として評価を付けてくれる確率が統計的に上がります。これは評価件数の増加、ひいてはアカウント信頼性の向上につながります。

つまり、フィードバック管理の自動化は「事後処理」ではなく、アカウント健康度を維持するための「能動的なアカウント経営」です。手動管理ではスケールに限界があり、数百件・数千件の注文を処理するセラーには自動化は必須要件です。

基本的な使い方(ベースライン):GetFeedback で評価一覧を取得する

まず、zeep ライブラリを使った SOAP 呼び出しで、セラーのフィードバック一覧を取得する最小構成の実装から始めます。GetFeedback はページネーションをサポートしており、大量の評価がある場合は複数ページに分けて取得する必要があります。

# get_feedback_baseline.py
import zeep
import zeep.transports
from dataclasses import dataclass
from typing import List, Optional

WSDL_URL = "https://api.ebay.com/wsapi?WSDL"  # 本番環境
# Sandbox: "https://api.sandbox.ebay.com/wsapi?WSDL"

@dataclass
class FeedbackEntry:
    feedback_id: str
    item_id: str
    transaction_id: str
    comment_type: str   # Positive / Neutral / Negative
    comment_text: str
    commenter_user_id: str
    role: str           # Seller / Buyer
    comment_time: Optional[str] = None


def get_feedback_list(
    auth_token: str,
    user_id: str,
    feedback_type: str = "FeedbackReceivedAsSeller",
    entries_per_page: int = 200,
) -> List[FeedbackEntry]:
    # GetFeedback API を呼び出し、指定ユーザーの評価一覧を全ページ取得する。
    # feedback_type: FeedbackReceivedAsSeller / FeedbackReceivedAsBuyer /
    #                FeedbackLeft / FeedbackReceived
    transport = zeep.transports.Transport(timeout=30)
    client = zeep.Client(wsdl=WSDL_URL, transport=transport)

    all_entries: List[FeedbackEntry] = []
    page_number = 1

    while True:
        request_body = {
            "RequesterCredentials": {"eBayAuthToken": auth_token},
            "UserID": user_id,
            "FeedbackType": feedback_type,
            "Pagination": {
                "EntriesPerPage": entries_per_page,
                "PageNumber": page_number,
            },
        }

        response = client.service.GetFeedback(**request_body)
        ack = getattr(response, "Ack", "Failure")

        if ack not in ("Success", "Warning"):
            errors = getattr(response, "Errors", [])
            raise RuntimeError(
                f"GetFeedback failed (Ack={ack}): "
                f"{[getattr(e, 'LongMessage', '') for e in errors]}"
            )

        feedback_detail_array = getattr(response, "FeedbackDetailArray", None)
        if not feedback_detail_array:
            break  # 評価なし、または全ページ取得完了

        details = getattr(feedback_detail_array, "FeedbackDetail", [])
        for detail in details:
            all_entries.append(
                FeedbackEntry(
                    feedback_id=str(getattr(detail, "FeedbackID", "")),
                    item_id=str(getattr(detail, "ItemID", "")),
                    transaction_id=str(getattr(detail, "TransactionID", "")),
                    comment_type=str(getattr(detail, "CommentType", "")),
                    comment_text=str(getattr(detail, "CommentText", "")),
                    commenter_user_id=str(getattr(detail, "CommentingUser", "")),
                    role=str(getattr(detail, "Role", "")),
                    comment_time=str(getattr(detail, "CommentTime", "")),
                )
            )

        # ページネーション: 次のページが存在するか確認
        pagination_result = getattr(response, "PaginationResult", None)
        if not pagination_result:
            break

        total_pages = getattr(pagination_result, "TotalNumberOfPages", 1)
        if page_number >= total_pages:
            break

        page_number += 1

    return all_entries


def analyze_feedback_score(entries: List[FeedbackEntry]) -> dict:
    # 取得した評価一覧を集計し、スコアを分析する。
    positive = sum(1 for e in entries if e.comment_type == "Positive")
    neutral  = sum(1 for e in entries if e.comment_type == "Neutral")
    negative = sum(1 for e in entries if e.comment_type == "Negative")
    total    = positive + neutral + negative

    positive_percentage = (positive / total * 100) if total > 0 else 0.0

    return {
        "positive_count": positive,
        "neutral_count": neutral,
        "negative_count": negative,
        "total_count": total,
        "positive_percentage": round(positive_percentage, 2),
        "is_top_rated_eligible": positive_percentage >= 98.0 and total >= 100,
    }


# ── 動作確認 ──
if __name__ == "__main__":
    TOKEN   = "YOUR_AUTH_TOKEN_HERE"
    USER_ID = "your_seller_id"

    entries  = get_feedback_list(TOKEN, USER_ID)
    analysis = analyze_feedback_score(entries)

    print(f"総評価数    : {analysis['total_count']}")
    print(f"Positive   : {analysis['positive_count']}")
    print(f"Neutral    : {analysis['neutral_count']}")
    print(f"Negative   : {analysis['negative_count']}")
    print(f"好評率      : {analysis['positive_percentage']}%")
    print(f"TRS 資格候補: {analysis['is_top_rated_eligible']}")
補足: FeedbackType パラメータの選択指針

GetFeedback の FeedbackType パラメータには4種類があり、用途によって使い分けます。「FeedbackReceivedAsSeller」は、バイヤーからセラーへの評価を取得します。アカウント健康度の監視や TRS チェックはこれを使います。「FeedbackLeft」は、自分がバイヤーに対して過去に残した評価の履歴を確認できます。二重送信チェックなどに利用します。「FeedbackReceived」は全タイプを混在取得しますが、フィルタリングコストが増えるため目的が明確な場合は専用タイプを選ぶことを推奨します。

補足: CommentType の3種類とスコアへの影響

FeedbackDetail の CommentType には Positive(好評)、Neutral(中立)、Negative(否定)の3種類があります。Feedback Score(累計スコア数)の計算式は「Positive件数 − Negative件数」であり、Neutral は加算も減算もされません。一方、Positive Feedback Percentage(好評率)は「Positive ÷ (Positive + Neutral + Negative) × 100」で算出されます。好評率においては Neutral も分母に含まれるため、Neutral が増えると好評率が下がる点に注意が必要です。

実務で躓く場面・深いポイント (Core)

ここからは、フィードバック自動化システムを実稼働させる上で、多くのエンジニアが必ず数時間単位でハマる3つの大きな落とし穴と、その回避策を解説します。

1. フィードバックを残せる「期限」と「タイミング」の落とし穴

eBay のフィードバックには、取引完了から 60日以内 という有効期限があります。これを超えた取引に対して LeaveFeedback を呼び出すと、Error 819("This transaction is not eligible for Feedback")が返ります。長い出品期間を持つ商品や、国際配送で時間がかかる商品を扱うセラーは、取引完了のタイムスタンプを必ず記録し、定期的に60日期限をチェックする仕組みが不可欠です。

しかし、さらに重要な問題があります。「取引完了 = 即座にフィードバックを送ってよい」という勘違いです。

eBay には、取引に対してケース(Case)や申請(Request)が開かれている場合があります。バイヤーが「商品が届かない(Item Not Received)」や「商品が説明と異なる(Not as Described)」という申請をオープンしている最中に Positive フィードバックを送ってしまうと、バイヤーにとって「セラーが一方的に問題を解決済みとみなそうとしている」という印象を与えかねません。これはむしろ Negative 評価を引き起こすリスクを高めます。

注意: 自動フィードバック送信のタイミングポリシー

eBay の公式ガイドラインでも、オープン中のケース・リクエスト・申請がある取引へのフィードバックは推奨されていません。堅牢な実装では、GetOrders API で取引のステータスを確認するか、発送確認(Shipped ステータス)から一定日数のバッファを設けてからフィードバックを送信するロジックが必要です。目安は国内配送で発送後7日、国際配送で発送後21日程度です。この「待機バッファ」のパラメータは設定値として外部化し、商品カテゴリや配送先国ごとに調整できる設計にしておくと実運用で柔軟に対応できます。

2. セラーはネガティブ評価をバイヤーに対して残せない(2008年ポリシー変更)

これは2008年の eBay ポリシー変更ですが、現在でも意外と知らないエンジニアが多い重要な仕様です。

2008年以降、セラーはバイヤーに対して Negative または Neutral の評価を残すことができません。LeaveFeedback の CommentType に "Negative" や "Neutral" を指定して送信すると、Error 821("You cannot leave a negative or neutral comment for a buyer")が返ります。

これは eBay が「セラーからの報復評価(Retaliatory Feedback)」を防ぐために意図的に設けた制約です。バイヤーがクレームを付けた後にセラーが Negative を返すことを恐れてバイヤーがクレームを躊躇するという悪循環を断ち切るための仕組みです。

この仕様を知らずに CommentType を動的に設定するスクリプトを組むと、バグの温床になります。セラーがバイヤーに送るフィードバックは、常に CommentType="Positive" にハードコードするのが正解です。CommentText(本文)のみをビジネスロジックに応じて動的に生成するアーキテクチャにすることで、このポリシー制約を構造的に守ることができます。

3. LeaveFeedback の二重送信ガードと eBay レート制限

LeaveFeedback には eBay 側のレート制限があります。同一ユーザーへのフィードバックは特定の時間窓内で回数制限があり、大量の注文を一括処理する際に無視すると途中から Error 21916588 が返り始めます。これは大量注文を一度にバルク処理するセラーが特に踏みやすい罠です。

同じく重大なのが二重送信です。同一取引(ItemID + TransactionID の組み合わせ)に対してフィードバックを2回送信しようとすると Error 819 が返り、2回目の送信は失敗します。eBay 側の保護機能があるとはいえ、「なぜ失敗したのか」をログで追跡できないシステムでは、単純な API エラーと区別がつかなくなります。

送信済みフィードバックを追跡するために、ローカル DB(SQLite、PostgreSQL、RDS 等)に(item_id, transaction_id, feedback_sent_at, status) のレコードを記録し、送信前にこのテーブルを参照する冪等性チェックが必須です。DB を挟むことで、API 呼び出し失敗時のリトライも安全に行えます。

頻出エラーコード早見表

LeaveFeedback / GetFeedback を実装する際に頻繁に遭遇するエラーコードをまとめます。

エラーコード メッセージ(略) 対処法
819 Transaction is not eligible for Feedback 取引完了から60日以上経過、または同一取引に送信済み。送信済みDBを確認し、期限切れはスキップする。
821 Cannot leave negative/neutral for a buyer セラーはバイヤーへ Negative/Neutral を送信不可。CommentType を常に "Positive" にハードコードする。
21916588 Rate limit exceeded for LeaveFeedback 短時間に大量送信でレート制限に到達。time.sleep() によるスロットリングを実装し、再試行は指数バックオフで行う。
1030 The specified user is suspended バイヤーのアカウントが停止状態。GetUser でユーザーステータスを事前確認してスキップ処理を追加する。
21917248 You cannot leave feedback for yourself テスト環境でセラーとバイヤーが同一 UserID のケース。Sandbox では別アカウントを用意する。

堅牢な実装:発送完了後に自動 Positive フィードバックを送るスクリプト

上記の落とし穴をすべてクリアした、本番稼働レベルの自動フィードバック送信システムを実装します。設計の要点は以下の3点です。

① ローカル DB(SQLite)による送信済みチェックで二重送信を防ぐ冪等性の確保。

② 発送完了からの経過日数バッファチェックにより、紛争中取引への誤送信を防ぐ。

③ レート制限対策のスロットリングと、指数バックオフによるリトライロジック。

# auto_feedback_sender.py
import sqlite3
import time
import logging
import zeep
import zeep.transports
from datetime import datetime, timezone, timedelta
from dataclasses import dataclass
from typing import List, Optional

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)
logger = logging.getLogger(__name__)

WSDL_URL = "https://api.ebay.com/wsapi?WSDL"


# ── データモデル ──
@dataclass
class ShippedOrder:
    # 発送済み注文情報(GetOrders などから取得したデータを想定)
    item_id: str
    transaction_id: str
    buyer_user_id: str
    shipped_at: datetime  # 発送完了のタイムスタンプ(UTC)
    order_id: str = ""


# ── DB管理: 送信済みフィードバック追跡 ──
def init_db(db_path: str = "feedback_tracker.db") -> sqlite3.Connection:
    # フィードバック送信履歴を管理する SQLite テーブルを初期化する。
    # ItemID + TransactionID をユニーク制約で管理し冪等性を保証する。
    conn = sqlite3.connect(db_path)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS sent_feedback (
            id              INTEGER PRIMARY KEY AUTOINCREMENT,
            item_id         TEXT NOT NULL,
            transaction_id  TEXT NOT NULL,
            buyer_user_id   TEXT NOT NULL,
            sent_at         TEXT NOT NULL,
            status          TEXT NOT NULL,
            error_message   TEXT,
            UNIQUE (item_id, transaction_id)
        )
    """)
    conn.commit()
    return conn


def is_feedback_already_sent(
    conn: sqlite3.Connection, item_id: str, transaction_id: str
) -> bool:
    # 指定取引のフィードバック送信済みかどうかをDBで確認する。
    cursor = conn.execute(
        "SELECT 1 FROM sent_feedback "
        "WHERE item_id = ? AND transaction_id = ? AND status = 'success'",
        (item_id, transaction_id),
    )
    return cursor.fetchone() is not None


def record_feedback_result(
    conn: sqlite3.Connection,
    item_id: str,
    transaction_id: str,
    buyer_user_id: str,
    status: str,
    error_message: Optional[str] = None,
) -> None:
    # フィードバック送信結果をDBに記録する。
    conn.execute(
        "INSERT OR REPLACE INTO sent_feedback "
        "(item_id, transaction_id, buyer_user_id, sent_at, status, error_message) "
        "VALUES (?, ?, ?, ?, ?, ?)",
        (
            item_id,
            transaction_id,
            buyer_user_id,
            datetime.now(timezone.utc).isoformat(),
            status,
            error_message,
        ),
    )
    conn.commit()


# ── フィードバック送信コア ──
def _build_comment_text(buyer_user_id: str) -> str:
    # 送信するフィードバックコメントを生成する。
    # eBay のガイドラインでは過度に定型的なコメントはフィルタされる場合があるため、
    # 簡潔で誠実なコメントを推奨する。
    return "Quick payment and smooth transaction. Highly recommended buyer! A++++"


def leave_positive_feedback(
    client: zeep.Client,
    auth_token: str,
    item_id: str,
    transaction_id: str,
    buyer_user_id: str,
    max_retries: int = 3,
) -> None:
    # 指定取引に対して Positive フィードバックを送信する。
    # 指数バックオフによるリトライロジックを内蔵する。

    # 注意: CommentType は常に "Positive" にハードコード
    # eBay ポリシー(2008年〜): セラーはバイヤーへ Negative/Neutral を送信不可
    request_body = {
        "RequesterCredentials": {"eBayAuthToken": auth_token},
        "ItemID": item_id,
        "TransactionID": transaction_id,
        "TargetUser": buyer_user_id,
        "CommentType": "Positive",
        "Comment": _build_comment_text(buyer_user_id),
    }

    last_error: Optional[Exception] = None

    for attempt in range(1, max_retries + 1):
        try:
            response = client.service.LeaveFeedback(**request_body)
            ack = getattr(response, "Ack", "Failure")

            if ack in ("Success", "Warning"):
                if ack == "Warning":
                    logger.warning(
                        f"LeaveFeedback succeeded with warnings "
                        f"(item={item_id}, tx={transaction_id})"
                    )
                logger.info(
                    f"Feedback sent: item={item_id}, tx={transaction_id}, "
                    f"buyer={buyer_user_id}"
                )
                return

            # エラーの詳細を解析
            errors = getattr(response, "Errors", [])
            error_codes = [str(getattr(e, "ErrorCode", "")) for e in errors]
            error_msgs  = [str(getattr(e, "LongMessage", "")) for e in errors]

            # 819: 既に送信済みまたは対象外 → リトライ不要
            if "819" in error_codes:
                raise RuntimeError(
                    f"Transaction not eligible (code=819): {error_msgs}. Skipping."
                )
            # 821: ポリシー違反 → リトライ不要
            if "821" in error_codes:
                raise RuntimeError(
                    "Policy violation (code=821): Cannot leave negative for buyer."
                )

            last_error = RuntimeError(
                f"LeaveFeedback failed (codes={error_codes}): {error_msgs}"
            )

        except RuntimeError:
            raise  # リトライ不要なエラーはそのまま再送出

        except Exception as exc:
            last_error = exc
            wait_sec = 2 ** attempt
            logger.warning(
                f"Attempt {attempt}/{max_retries} failed: {exc}. "
                f"Retrying in {wait_sec}s..."
            )
            time.sleep(wait_sec)  # 指数バックオフ: 2s, 4s, 8s

    raise RuntimeError(
        f"Max retries ({max_retries}) exceeded for item={item_id}, "
        f"tx={transaction_id}. Last error: {last_error}"
    )


# ── メインループ: バッチ処理 ──
def run_feedback_batch(
    auth_token: str,
    shipped_orders: List[ShippedOrder],
    shipping_buffer_days: int = 7,
    inter_request_sleep: float = 1.0,
    db_path: str = "feedback_tracker.db",
) -> dict:
    # 発送済み注文リストに対して、条件を満たすものに一括で Positive フィードバックを送る。
    # shipping_buffer_days: 発送完了から待機する日数(紛争ケース対策)
    # inter_request_sleep: API リクエスト間のスリープ秒数(レート制限対策)

    conn       = init_db(db_path)
    transport  = zeep.transports.Transport(timeout=30)
    client     = zeep.Client(wsdl=WSDL_URL, transport=transport)

    now_utc          = datetime.now(timezone.utc)
    buffer_threshold = timedelta(days=shipping_buffer_days)
    # 60日制限に2日の安全マージンを設ける
    feedback_expiry  = timedelta(days=58)

    results = {
        "success": 0, "skipped_buffer": 0,
        "skipped_sent": 0, "skipped_expired": 0, "failed": 0,
    }

    for order in shipped_orders:
        item_id    = order.item_id
        tx_id      = order.transaction_id
        buyer      = order.buyer_user_id
        shipped_at = order.shipped_at

        if shipped_at.tzinfo is None:
            shipped_at = shipped_at.replace(tzinfo=timezone.utc)

        elapsed = now_utc - shipped_at

        # 1. バッファ期間内: 発送直後はスキップ(紛争ケース対策)
        if elapsed < buffer_threshold:
            logger.debug(
                f"Skipping (buffer): item={item_id}, elapsed={elapsed.days}d"
            )
            results["skipped_buffer"] += 1
            continue

        # 2. 期限切れ: 58日超はスキップ
        if elapsed > feedback_expiry:
            logger.warning(
                f"Skipping (expired): item={item_id}, elapsed={elapsed.days}d"
            )
            results["skipped_expired"] += 1
            continue

        # 3. DB に送信済み記録あり: スキップ(冪等性チェック)
        if is_feedback_already_sent(conn, item_id, tx_id):
            logger.debug(f"Skipping (already sent): item={item_id}, tx={tx_id}")
            results["skipped_sent"] += 1
            continue

        # フィードバック送信
        try:
            leave_positive_feedback(client, auth_token, item_id, tx_id, buyer)
            record_feedback_result(conn, item_id, tx_id, buyer, "success")
            results["success"] += 1

        except Exception as exc:
            error_msg = str(exc)
            logger.error(
                f"Failed: item={item_id}, tx={tx_id}. Error: {error_msg}"
            )
            record_feedback_result(conn, item_id, tx_id, buyer, "failed", error_msg)
            results["failed"] += 1

        finally:
            # レート制限対策: リクエスト間に必ずスリープを挟む
            time.sleep(inter_request_sleep)

    conn.close()
    logger.info(f"Feedback batch completed: {results}")
    return results


# ── エントリーポイント(動作確認用)──
if __name__ == "__main__":
    TOKEN = "YOUR_AUTH_TOKEN_HERE"

    # 実際には GetOrders API から取得した発送済み注文リストを使用する
    sample_orders = [
        ShippedOrder(
            item_id="123456789012",
            transaction_id="9876543210",
            buyer_user_id="sample_buyer_01",
            shipped_at=datetime.now(timezone.utc) - timedelta(days=10),
        ),
        ShippedOrder(
            item_id="987654321098",
            transaction_id="1234567890",
            buyer_user_id="sample_buyer_02",
            shipped_at=datetime.now(timezone.utc) - timedelta(days=3),  # バッファ内
        ),
    ]

    summary = run_feedback_batch(
        auth_token=TOKEN,
        shipped_orders=sample_orders,
        shipping_buffer_days=7,
        inter_request_sleep=1.5,
    )
    print(f"実行結果: {summary}")
補足: GetOrders API との連携

run_feedback_batch 関数は ShippedOrder のリストを引数に受け取ります。実際のシステムでは、GetOrders API(または REST の Orders API)で発送完了(Shipped)ステータスの注文を定期取得し、ShippedOrder オブジェクトに変換してこの関数に渡します。スケジューラー(cron、Celery Beat、AWS EventBridge 等)で1日1〜2回このバッチを実行するのが一般的な運用パターンです。DB の sent_feedback テーブルが冪等性を保証するため、複数回実行しても二重送信は発生しません。

パフォーマンス・スケーリング視点 (深度)

大量注文のスロットリング戦略と定期フィードバックレポート

月間数千件以上の注文を処理する大規模セラーにとって、フィードバック管理のアーキテクチャには追加の考慮が必要です。

【スロットリングの設計】

eBay の LeaveFeedback は1日あたりのコール数制限があります。大量の注文が一度に発生した場合(セール期間など)、単純なループ処理ではレート制限に達してしまいます。実務では以下のアーキテクチャを推奨します。

まず、対象注文をキュー(RabbitMQ、AWS SQS、Redis Queue 等)に投入します。コンシューマーはキューからメッセージを取り出し、各リクエスト間に必ず inter_request_sleep 秒を確保して順次処理します。キューの深さ(メッセージ数)をモニタリングすることで、バックログの蓄積状況をリアルタイムに把握できます。突発的な大量注文でも、キューがバッファとして機能するためレート制限エラーを回避できます。

【定期フィードバックレポートと健康度監視】

GetFeedback で週次または月次に評価データを取得し、Positive Percentage のトレンドを DB に蓄積していくことで、アカウント健康度の変化を早期に検知できます。Positive Percentage が 98.5% を下回ったらアラート通知(Slack、メール等)を送る仕組みを設けることで、TRS ラインの 98.0% を割り込む前に対処できます。

# feedback_health_monitor.py
# 週次レポート生成と健康度アラートの実装例

import sqlite3
from datetime import datetime, timezone


def record_weekly_score(
    db_path: str,
    positive_count: int,
    neutral_count: int,
    negative_count: int,
) -> None:
    # 週次フィードバックスコアを時系列DBに記録する。
    # 蓄積データにより Positive Percentage のトレンドグラフを描画可能にする。
    conn = sqlite3.connect(db_path)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS feedback_history (
            recorded_at         TEXT NOT NULL,
            positive_count      INTEGER,
            neutral_count       INTEGER,
            negative_count      INTEGER,
            positive_percentage REAL
        )
    """)
    total = positive_count + neutral_count + negative_count
    pct   = round(positive_count / total * 100, 2) if total > 0 else 0.0

    conn.execute(
        "INSERT INTO feedback_history VALUES (?, ?, ?, ?, ?)",
        (
            datetime.now(timezone.utc).isoformat(),
            positive_count, neutral_count, negative_count, pct,
        ),
    )
    conn.commit()
    conn.close()


def check_health_alert(
    positive_percentage: float, threshold: float = 98.5
) -> bool:
    # Positive Percentage が閾値を下回った場合にアラートを返す。
    # 閾値を TRS 基準(98.0%)より高めに設定することで早期警告を実現する。
    if positive_percentage < threshold:
        print(
            f"[ALERT] Positive Percentage {positive_percentage}% "
            f"has dropped below threshold {threshold}%. "
            "Immediate review of recent Negative feedbacks is recommended."
        )
        # 本番では Slack Webhook / SNS / PagerDuty 等に通知を送る
        return True
    return False


# ── 使用例 ──
if __name__ == "__main__":
    TOKEN   = "YOUR_AUTH_TOKEN_HERE"
    USER_ID = "your_seller_id"

    # 1. GetFeedback で現在のスコアを取得
    from get_feedback_baseline import get_feedback_list, analyze_feedback_score
    entries  = get_feedback_list(TOKEN, USER_ID)
    analysis = analyze_feedback_score(entries)

    # 2. 週次スコアを時系列DBに記録
    record_weekly_score(
        db_path="health_history.db",
        positive_count=analysis["positive_count"],
        neutral_count=analysis["neutral_count"],
        negative_count=analysis["negative_count"],
    )

    # 3. 健康度アラートを確認
    is_alert = check_health_alert(analysis["positive_percentage"], threshold=98.5)
    if is_alert:
        print("Action required: review recent Negative feedbacks in Seller Hub.")
    else:
        print(f"Account health is good: {analysis['positive_percentage']}%")

GetFeedback の FeedbackSummary フィールドには、FeedbackScore(累計スコア)、PositiveFeedbackPercent(好評率)、そして直近1ヶ月・6ヶ月・12ヶ月の期間別サマリーが含まれています。これらを週次で記録することで、SQLite または PostgreSQL による時系列トレンド分析が可能になり、Negative 評価のスパイクを可視化・早期検知できます。

大規模運用における最終的なアーキテクチャとしては、①GetOrders(日次バッチ)→ ②キューイング → ③スロットリング付き LeaveFeedback → ④健康度スコアの時系列記録 → ⑤閾値アラートという5段構成が理想的です。この構成により、人手を介さずともアカウント健康度を高水準に維持し続けられます。

まとめ

本記事では、eBay セラーのアカウント健康度管理において核心的な役割を果たすフィードバック API の全体像を実装しました。

  • ベースライン: GetFeedback で評価一覧を全件ページネーション取得し、Positive / Neutral / Negative 別に集計して Positive Percentage を算出する Python 実装を構築しました。FeedbackType と CommentType の正確な理解が出発点です。
  • 深いポイント: 60日有効期限と紛争中取引への誤送信リスク、2008年ポリシー変更によるセラーの CommentType 制約(Positive のみ可)、そして DB 管理による二重送信ガードの3点が、自動化システムの信頼性を決定づける要因です。これらを見落とすと本番環境でサイレントにエラーが積み重なります。
  • スケーリング: キューイング+スロットリングによるレート制限対策と、週次スコアの時系列記録による早期健康度アラートを組み合わせることで、数千件規模の注文をこなす大規模セラーでも安定運用できるアーキテクチャが完成します。

本記事をもって、第2回から始まった Trading API 連載(全16回)が完結します。GeteBayDetails でのメタデータ取得(#2, #3)に始まり、画像アップロード(#3)、単一商品・バリエーション出品(#4, #6)、在庫管理(#7〜#9)、注文管理(#10〜#12)、返品・クレーム対応(#13〜#15)、カテゴリ情報取得(#16)、そして本記事のフィードバック管理(#17)まで、Trading API の実務的な全領域をカバーしました。これらを組み合わせることで、出品から販売・アフターケアまでを Python で完全自動化する基盤が整いました。

次のステップ

次回(#18)から、本連載は大きな技術的転換点を迎えます。「Inventory Mapping API 入門:Python と GraphQL で AI 推奨の出品プレビューを作成する」と題し、連載42回シリーズで初めて GraphQL API が登場します。これまでの Trading API(SOAP/XML)から、よりモダンで型安全な GraphQL ベースの eBay 新世代 API へと舞台が移ります。

GraphQL のクエリ構造、Python からの呼び出し方、そして AI 推奨カテゴリのプレビュー取得まで、新しいパラダイムをゼロから丁寧に解説します。Trading API で培った実装力を武器に、次のステージへ進みましょう。お楽しみに!

次の記事はこちら

トップに戻る