feat: add daily manual price review workflow
This commit is contained in:
102
arr_web/app.py
102
arr_web/app.py
@@ -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:])
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user