159 lines
4.8 KiB
Python
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",
|
|
)
|