feat: add daily manual price review workflow

This commit is contained in:
Wyndham ARR
2026-08-06 22:40:18 +08:00
parent aae3d8e1db
commit ca3e8e18fa
77 changed files with 7750 additions and 602 deletions

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import base64
import json
import re
import secrets
import threading
import time
@@ -44,7 +45,9 @@ from arr_web.company_jobs import (
UnavailableCompanyReportCoordinator,
)
from arr_web.services import (
DailyPriceReviewCoordinator,
MonthlyCoordinator,
UnavailableDailyPriceReviewCoordinator,
UnavailableMonthlyCoordinator,
UnavailableUploadCoordinator,
UploadCoordinator,
@@ -234,6 +237,7 @@ class PortalApplication:
self,
repository: Optional[PortalRepository] = None,
upload: Optional[UploadCoordinator] = None,
price_reviews: Optional[DailyPriceReviewCoordinator] = None,
monthly: Optional[MonthlyCoordinator] = None,
company_reports: Optional[CompanyReportCoordinator] = None,
booking_sources: Optional[BookingSourceCoordinator] = None,
@@ -248,6 +252,7 @@ class PortalApplication:
) -> None:
self._repository = repository or UnavailablePortalRepository()
self._upload = upload or UnavailableUploadCoordinator()
self._price_reviews = price_reviews or UnavailableDailyPriceReviewCoordinator()
self._monthly = monthly or UnavailableMonthlyCoordinator()
self._company_reports = (
company_reports or UnavailableCompanyReportCoordinator()
@@ -376,6 +381,20 @@ class PortalApplication:
200,
success(self._repository.get_job_trace(job_id)),
)
if (
method == "GET"
and len(job_parts) == 4
and job_parts[:2] == ["api", "jobs"]
and job_parts[3] == "review"
):
job_id = validate_job_id(job_parts[2])
query = parse_qs(route.query, keep_blank_values=True)
limit = self._integer(query, "limit", 50, 1, 200)
offset = self._integer(query, "offset", 0, 0, 10_000_000)
return Response.json(
200,
success(self._price_reviews.get_price_review(job_id, limit, offset)),
)
if method == "GET" and route.path == "/api/monthly-runs":
query = parse_qs(route.query, keep_blank_values=True)
month = validate_month(self._one(query, "month", self._current_month()))
@@ -554,6 +573,80 @@ class PortalApplication:
filename = validate_upload_filename(filename)
validate_xml_payload(body)
return Response.json(202, success(self._upload.submit(filename, body)))
if (
method == "PATCH"
and len(job_parts) == 6
and job_parts[:2] == ["api", "jobs"]
and job_parts[3:5] == ["review", "items"]
):
self._require_csrf(normalized_headers)
job_id = validate_job_id(job_parts[2])
try:
item_id = int(job_parts[5])
except ValueError:
raise PortalError("REVIEW_ITEM_NOT_FOUND", "待人工处理项不存在", 404) from None
if item_id < 1 or len(body) > 8192:
raise PortalError("REVIEW_REQUEST_INVALID", "复核请求字段无效")
payload = _strict_json(body)
if set(payload) != {"case_id", "revision", "real_price"}:
raise PortalError("REVIEW_REQUEST_INVALID", "复核请求字段无效")
case_id = payload.get("case_id")
revision = payload.get("revision")
real_price = payload.get("real_price")
if (
not self._valid_review_case_id(case_id)
or not isinstance(revision, int)
or isinstance(revision, bool)
or revision < 0
or not isinstance(real_price, str)
or re.fullmatch(r"(?:0|[1-9][0-9]{0,15})", real_price) is None
):
raise PortalError("REVIEW_REQUEST_INVALID", "复核请求字段无效")
return Response.json(
200,
success(
self._price_reviews.update_price_review_item(
job_id,
item_id,
case_id,
revision,
real_price,
current_session[0],
)
),
)
if (
method in {"POST"}
and len(job_parts) == 5
and job_parts[:2] == ["api", "jobs"]
and job_parts[3] == "review"
and job_parts[4] in {"finalize", "cancel"}
):
self._require_csrf(normalized_headers)
job_id = validate_job_id(job_parts[2])
if len(body) > 8192:
raise PortalError("REVIEW_REQUEST_INVALID", "复核请求字段无效")
payload = _strict_json(body)
if set(payload) != {"case_id", "revision"}:
raise PortalError("REVIEW_REQUEST_INVALID", "复核请求字段无效")
case_id = payload.get("case_id")
revision = payload.get("revision")
if (
not self._valid_review_case_id(case_id)
or not isinstance(revision, int)
or isinstance(revision, bool)
or revision < 0
):
raise PortalError("REVIEW_REQUEST_INVALID", "复核请求字段无效")
if job_parts[4] == "finalize":
result = self._price_reviews.finalize_price_review(
job_id, case_id, revision, current_session[0]
)
else:
result = self._price_reviews.cancel_price_review(
job_id, case_id, revision, current_session[0]
)
return Response.json(200, success(result))
if method == "POST" and route.path == "/api/company-reports/source":
self._require_csrf(normalized_headers)
filename = _decode_filename_header(
@@ -924,3 +1017,12 @@ class PortalApplication:
if not minimum <= value <= maximum:
raise PortalError("QUERY_INVALID", "查询参数无效")
return value
@staticmethod
def _valid_review_case_id(value: Any) -> bool:
return (
isinstance(value, str)
and len(value) == 44
and value.startswith("dailyreview-")
and all(character in "0123456789abcdef" for character in value[12:])
)