Files
ewoooc/services/pchome_sales_freshness_policy.py

159 lines
4.8 KiB
Python

"""Taipei-time availability SLA for PChome daily sales reports."""
from __future__ import annotations
import os
from datetime import date, datetime, time, timedelta
from typing import Any
import pytz
CONTRACT_VERSION = "pchome_sales_freshness_sla_v1"
TAIPEI_TZ = pytz.timezone("Asia/Taipei")
DEFAULT_REPORT_CUTOFF_HOUR = 20
def _bounded_cutoff_hour(value: Any = None) -> int:
raw = os.getenv("PCHOME_SALES_REPORT_CUTOFF_HOUR", "20") if value is None else value
try:
parsed = int(raw)
except (TypeError, ValueError):
parsed = DEFAULT_REPORT_CUTOFF_HOUR
return max(0, min(23, parsed))
def _taipei_now(value: datetime | None) -> datetime:
if value is None:
return datetime.now(TAIPEI_TZ)
if value.tzinfo is None:
return TAIPEI_TZ.localize(value)
return value.astimezone(TAIPEI_TZ)
def _parse_sales_date(value: Any) -> date | None:
if isinstance(value, datetime):
return value.date()
if isinstance(value, date):
return value
raw = str(value or "").strip()
if not raw:
return None
try:
return date.fromisoformat(raw[:10])
except ValueError:
return None
def evaluate_pchome_sales_freshness(
latest_sales_date: Any,
*,
now: datetime | None = None,
cutoff_hour: int | None = None,
) -> dict[str, Any]:
"""Evaluate freshness against the report arrival deadline, not midnight."""
observed_at = _taipei_now(now)
bounded_cutoff = _bounded_cutoff_hour(cutoff_hour)
report_due_at = TAIPEI_TZ.localize(
datetime.combine(observed_at.date(), time(hour=bounded_cutoff))
)
arrival_window_open = observed_at < report_due_at
expected_latest_date = observed_at.date() - timedelta(
days=2 if arrival_window_open else 1
)
raw = str(latest_sales_date or "").strip()
parsed = _parse_sales_date(latest_sales_date)
common = {
"contract_version": CONTRACT_VERSION,
"observed_at": observed_at.isoformat(),
"report_timezone": "Asia/Taipei",
"report_cutoff_hour": bounded_cutoff,
"report_due_at": report_due_at.isoformat(),
"arrival_window_open": arrival_window_open,
"expected_latest_sales_date": expected_latest_date.isoformat(),
}
if not raw:
return {
**common,
"latest_sales_date": None,
"status": "missing",
"freshness_status": "missing",
"label": "尚無業績資料",
"age_days": None,
"data_lag_days": None,
"sla_lag_days": None,
"grace_period_active": False,
"decision_ready": False,
"requires_upstream_acquisition": True,
"next_action": "自動取得並匯入最新 PChome 業績檔",
}
if parsed is None:
return {
**common,
"latest_sales_date": raw[:10],
"status": "invalid",
"freshness_status": "invalid",
"label": "業績日期格式異常",
"age_days": None,
"data_lag_days": None,
"sla_lag_days": None,
"grace_period_active": False,
"decision_ready": False,
"requires_upstream_acquisition": True,
"next_action": "修正業績資料日期後重新匯入",
}
age_days = (observed_at.date() - parsed).days
sla_lag_days = (expected_latest_date - parsed).days
if age_days < 0:
status = "future"
label = "業績日期超前"
ready = False
elif sla_lag_days <= 0:
ready = True
if arrival_window_open and age_days == 2:
status = "grace"
label = "到檔 SLA 內"
else:
status = "fresh"
label = "資料新鮮"
elif sla_lag_days == 1:
status = "warning"
label = "資料超過到檔 SLA"
ready = False
else:
status = "critical"
label = "資料已過期"
ready = False
grace_period_active = bool(ready and status == "grace")
return {
**common,
"latest_sales_date": parsed.isoformat(),
"status": status,
"freshness_status": status,
"label": label,
"age_days": max(0, age_days),
"data_lag_days": max(0, age_days),
"sla_lag_days": sla_lag_days,
"grace_period_active": grace_period_active,
"decision_ready": ready,
"requires_upstream_acquisition": not ready,
"next_action": (
"持續自動探測,於到檔 SLA 前使用最近完整日資料"
if grace_period_active
else "依最新業績執行今日作戰清單"
if ready
else "自動取得並匯入最新 PChome 業績檔"
),
}
__all__ = (
"CONTRACT_VERSION",
"DEFAULT_REPORT_CUTOFF_HOUR",
"TAIPEI_TZ",
"evaluate_pchome_sales_freshness",
)