まず前提の整理です。ASC API は全リクエストに ES256 署名の JWT を要求し、トークンの有効期限は最大20分です。Sales and Trends API は日次レポートを .tsv.gz で返しますが、前日分が確定するのは太平洋時間の早朝で、日本時間だと当日夜〜翌日昼にずれ込むことがあります。そしてレート制限の具体値は公開されていません。この3つの仕様が、それぞれ 401・404・429 の欠損に対応します。
create table asc_fetch_log ( report_date date primary key, status text not null check (status in ('pending', 'fetched', 'gave_up')), attempts int not null default 0, last_attempt_at timestamptz, row_count int, note text);
// supabase/functions/fetch-sales/index.ts(要点のみ)const BACKFILL_HOURS = 72;async function fetchWithLedger(reportDate: string) { const token = await getAscToken(); const res = await requestSalesReport(token, reportDate); if (res.status === 404) { const hoursSince = hoursSinceReportDate(reportDate); // 72時間は「未確定」として翌実行で再試行。それを超えたら人間に通知 const status = hoursSince < BACKFILL_HOURS ? "pending" : "gave_up"; await upsertLog(reportDate, { status, note: "404 from Sales API" }); if (status === "gave_up") await notifySlack(`売上レポート未取得: ${reportDate}`); return; } const rows = await parseTsvGz(res); await saveSales(reportDate, rows); await upsertLog(reportDate, { status: "fetched", row_count: rows.length });}// 実行のたびに、直近7日の pending をまとめて再試行するasync function backfillPending() { const { data } = await supabase .from("asc_fetch_log") .select("report_date") .eq("status", "pending") .gte("report_date", isoDaysAgo(7)); for (const row of data ?? []) { await fetchWithLedger(row.report_date); await sleep(200); // レート制限への予防 }}
ポイントは、日次ジョブの仕事を「今日の分を取る」から「期待される日付のうち、まだ取れていないものを取る」に変えたことです。取得対象を日付起点にすると、確定遅延もジョブの一時的な失敗も、同じバックフィルの仕組みで自然に埋まります。72時間で gave_up に落として通知するのは、vendor number の間違いのような「待っても解決しない404」を無限に再試行しないためです。
-- 過去30日で「売上行が1件もない日」を列挙するwith expected as ( select generate_series( current_date - interval '30 days', current_date - interval '2 days', -- 直近1日は未確定なので除外 interval '1 day' )::date as report_date)select e.report_date, coalesce(l.status, 'no_log') as log_statusfrom expected eleft join asc_fetch_log l using (report_date)left join ( select report_date, count(*) as cnt from app_daily_sales group by report_date) s using (report_date)where coalesce(s.cnt, 0) = 0order by e.report_date;