fix: 简化待付款订单核单条件 (#126)
This commit is contained in:
@@ -18,6 +18,7 @@ class PurchaseReconcileQuery:
|
||||
unit_price_cent: int
|
||||
total_price_cent: int
|
||||
irreversible_action_at: str
|
||||
order_submitted_at: str
|
||||
reconcile_started_at: str
|
||||
|
||||
|
||||
|
||||
@@ -208,21 +208,14 @@ class PurchaseReconcileService:
|
||||
) -> bool:
|
||||
if (
|
||||
not candidate.order_no.strip()
|
||||
or candidate.goods_id.strip() != query.goods_id
|
||||
or dict(candidate.options) != dict(query.options)
|
||||
or candidate.quantity != query.quantity
|
||||
or candidate.total_price_cent != query.total_price_cent
|
||||
or candidate.payment_status != "unpaid"
|
||||
):
|
||||
return False
|
||||
try:
|
||||
ordered_at = _parse_iso_time(candidate.ordered_at)
|
||||
lower = _parse_iso_time(query.irreversible_action_at) - timedelta(
|
||||
minutes=5
|
||||
)
|
||||
upper = _parse_iso_time(query.reconcile_started_at) + timedelta(
|
||||
minutes=5
|
||||
)
|
||||
submitted_at = _parse_iso_time(query.order_submitted_at)
|
||||
lower = submitted_at - timedelta(minutes=5)
|
||||
upper = submitted_at + timedelta(minutes=5)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return lower <= ordered_at <= upper
|
||||
@@ -237,21 +230,41 @@ class PurchaseReconcileService:
|
||||
options = payload.get("options")
|
||||
if not isinstance(options, Mapping) or not options:
|
||||
raise ValueError("采购任务缺少 options")
|
||||
requested_options = {
|
||||
str(key): str(value) for key, value in options.items()
|
||||
}
|
||||
goods_id = str(payload.get("goods_id") or "").strip()
|
||||
quantity = payload.get("quantity")
|
||||
requested_quantity = payload.get("quantity")
|
||||
snapshot = run.diagnostics_json.get("final_confirmation")
|
||||
if not isinstance(snapshot, Mapping):
|
||||
raise ValueError("采购运行缺少提交前确认快照")
|
||||
confirmed_options = snapshot.get("options")
|
||||
confirmed_quantity = snapshot.get("quantity")
|
||||
total_price_cent = snapshot.get("total_price_cent")
|
||||
unit_price_cent = snapshot.get("unit_price_cent")
|
||||
if not goods_id:
|
||||
raise ValueError("采购任务缺少 goods_id")
|
||||
if (
|
||||
isinstance(quantity, bool)
|
||||
or not isinstance(quantity, int)
|
||||
or quantity <= 0
|
||||
isinstance(requested_quantity, bool)
|
||||
or not isinstance(requested_quantity, int)
|
||||
or requested_quantity <= 0
|
||||
):
|
||||
raise ValueError("采购任务缺少有效 quantity")
|
||||
if not isinstance(confirmed_options, Mapping):
|
||||
raise ValueError("采购运行缺少确认规格")
|
||||
normalized_confirmed_options = {
|
||||
str(key): str(value) for key, value in confirmed_options.items()
|
||||
}
|
||||
if normalized_confirmed_options != requested_options:
|
||||
raise ValueError("采购运行确认规格与任务不一致")
|
||||
if (
|
||||
isinstance(confirmed_quantity, bool)
|
||||
or not isinstance(confirmed_quantity, int)
|
||||
or confirmed_quantity <= 0
|
||||
):
|
||||
raise ValueError("采购运行缺少有效确认数量")
|
||||
if confirmed_quantity != requested_quantity:
|
||||
raise ValueError("采购运行确认数量与任务不一致")
|
||||
if (
|
||||
isinstance(total_price_cent, bool)
|
||||
or not isinstance(total_price_cent, int)
|
||||
@@ -264,13 +277,19 @@ class PurchaseReconcileService:
|
||||
or unit_price_cent <= 0
|
||||
):
|
||||
raise ValueError("采购运行缺少有效确认单价")
|
||||
order_submitted_at = str(
|
||||
run.order_submitted_at or run.irreversible_action_at or ""
|
||||
).strip()
|
||||
if not order_submitted_at:
|
||||
raise ValueError("采购运行缺少下单时间基准")
|
||||
return PurchaseReconcileQuery(
|
||||
goods_id=goods_id,
|
||||
options={str(key): str(value) for key, value in options.items()},
|
||||
quantity=quantity,
|
||||
options=normalized_confirmed_options,
|
||||
quantity=confirmed_quantity,
|
||||
unit_price_cent=unit_price_cent,
|
||||
total_price_cent=total_price_cent,
|
||||
irreversible_action_at=str(run.irreversible_action_at),
|
||||
order_submitted_at=order_submitted_at,
|
||||
reconcile_started_at=_utc_now_iso(),
|
||||
)
|
||||
|
||||
@@ -292,10 +311,10 @@ class PurchaseReconcileService:
|
||||
"max_price_cent": task.price_cent,
|
||||
},
|
||||
"confirmed": {
|
||||
"options": dict(candidate.options),
|
||||
"quantity": candidate.quantity,
|
||||
"options": dict(query.options),
|
||||
"quantity": query.quantity,
|
||||
"unit_price_cent": query.unit_price_cent,
|
||||
"total_price_cent": candidate.total_price_cent,
|
||||
"total_price_cent": query.total_price_cent,
|
||||
},
|
||||
"confirmation_reached": True,
|
||||
"order_submitted": True,
|
||||
|
||||
@@ -26,6 +26,7 @@ def query() -> PurchaseReconcileQuery:
|
||||
unit_price_cent=4200,
|
||||
total_price_cent=8400,
|
||||
irreversible_action_at="2026-08-10T08:00:00Z",
|
||||
order_submitted_at="2026-08-10T08:03:00Z",
|
||||
reconcile_started_at="2026-08-10T08:05:00Z",
|
||||
)
|
||||
|
||||
@@ -235,6 +236,29 @@ class PddU2PurchaseReconcileAdapterTest(unittest.TestCase):
|
||||
|
||||
self.assertIsNone(_merge_order_evidence(first, second))
|
||||
|
||||
def test_minimal_order_and_time_candidate_uses_five_minute_window(self):
|
||||
within_window = PurchaseOrderCandidate(
|
||||
order_no="ORDER-MINIMAL",
|
||||
ordered_at="2026-08-10T08:08:00Z",
|
||||
payment_status="unpaid",
|
||||
)
|
||||
outside_window = PurchaseOrderCandidate(
|
||||
order_no="ORDER-LATE",
|
||||
ordered_at="2026-08-10T08:08:01Z",
|
||||
payment_status="unpaid",
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
PurchaseReconcileService._candidate_matches(
|
||||
within_window, query()
|
||||
)
|
||||
)
|
||||
self.assertFalse(
|
||||
PurchaseReconcileService._candidate_matches(
|
||||
outside_window, query()
|
||||
)
|
||||
)
|
||||
|
||||
def test_multiple_order_actions_disable_detail_evidence_merge(self):
|
||||
root = _parse_xml(
|
||||
'<hierarchy><node package="com.xunmeng.pinduoduo" '
|
||||
|
||||
@@ -99,10 +99,13 @@ class FaultAdapter(PddPurchaseAdapter):
|
||||
|
||||
|
||||
class ReadOnlyReconcileAdapter(PddPurchaseReconcileAdapter):
|
||||
def __init__(self, calls, candidates=None, error=None) -> None:
|
||||
def __init__(
|
||||
self, calls, candidates=None, error=None, preserve_times=False
|
||||
) -> None:
|
||||
self.calls = calls
|
||||
self.candidates = candidates
|
||||
self.error = error
|
||||
self.preserve_times = preserve_times
|
||||
|
||||
def read_order_candidates(self, query):
|
||||
self.calls.append(("reconcile", query.goods_id))
|
||||
@@ -113,18 +116,14 @@ class ReadOnlyReconcileAdapter(PddPurchaseReconcileAdapter):
|
||||
candidates = (
|
||||
PurchaseOrderCandidate(
|
||||
order_no="ORDER-001",
|
||||
goods_id=query.goods_id,
|
||||
options=dict(query.options),
|
||||
quantity=query.quantity,
|
||||
total_price_cent=query.total_price_cent,
|
||||
ordered_at=query.irreversible_action_at,
|
||||
ordered_at=query.order_submitted_at,
|
||||
ordered_at_raw="2026-08-10 16:00:00",
|
||||
payment_status="unpaid",
|
||||
),
|
||||
)
|
||||
else:
|
||||
elif not self.preserve_times:
|
||||
candidates = tuple(
|
||||
replace(candidate, ordered_at=query.irreversible_action_at)
|
||||
replace(candidate, ordered_at=query.order_submitted_at)
|
||||
for candidate in candidates
|
||||
)
|
||||
return PurchaseReconcileScan(
|
||||
@@ -305,6 +304,15 @@ class PurchaseRecoveryTest(unittest.TestCase):
|
||||
detail.pdd_data["purchase"]["payment_status"], "unpaid"
|
||||
)
|
||||
self.assertEqual(detail.pdd_data["purchase"]["order_no"], "ORDER-001")
|
||||
self.assertEqual(
|
||||
detail.pdd_data["purchase"]["confirmed"],
|
||||
{
|
||||
"options": OPTIONS,
|
||||
"quantity": 2,
|
||||
"unit_price_cent": 4200,
|
||||
"total_price_cent": 8400,
|
||||
},
|
||||
)
|
||||
|
||||
def test_reconcile_device_failure_is_recorded_as_unknown(self):
|
||||
task_id = "PUR-RECONCILE-OFFLINE"
|
||||
@@ -342,7 +350,7 @@ class PurchaseRecoveryTest(unittest.TestCase):
|
||||
|
||||
def test_no_multiple_mismatched_and_paid_candidates_need_manual_review(self):
|
||||
cases = {
|
||||
"EMPTY": ((), "ORDER_NOT_FOUND"),
|
||||
"EMPTY": ((), "ORDER_NOT_FOUND", False),
|
||||
"MULTIPLE": (
|
||||
(
|
||||
PurchaseOrderCandidate(
|
||||
@@ -367,21 +375,19 @@ class PurchaseRecoveryTest(unittest.TestCase):
|
||||
),
|
||||
),
|
||||
"AMBIGUOUS_ORDER_MATCH",
|
||||
False,
|
||||
),
|
||||
"MISMATCH": (
|
||||
"OUT_OF_TIME": (
|
||||
(
|
||||
PurchaseOrderCandidate(
|
||||
"ORDER-C",
|
||||
"OTHER-GOODS",
|
||||
OPTIONS,
|
||||
2,
|
||||
8400,
|
||||
"2026-08-10T08:00:00Z",
|
||||
"2026-08-10 16:00:00",
|
||||
"unpaid",
|
||||
order_no="ORDER-C",
|
||||
ordered_at="2020-01-01T00:00:00Z",
|
||||
ordered_at_raw="2020-01-01 08:00:00",
|
||||
payment_status="unpaid",
|
||||
),
|
||||
),
|
||||
"ORDER_MATCH_UNCERTAIN",
|
||||
True,
|
||||
),
|
||||
"PAID": (
|
||||
(
|
||||
@@ -397,9 +403,31 @@ class PurchaseRecoveryTest(unittest.TestCase):
|
||||
),
|
||||
),
|
||||
"ORDER_MATCH_UNCERTAIN",
|
||||
False,
|
||||
),
|
||||
"MISSING_ORDER_NO": (
|
||||
(
|
||||
PurchaseOrderCandidate(
|
||||
order_no="",
|
||||
payment_status="unpaid",
|
||||
),
|
||||
),
|
||||
"ORDER_MATCH_UNCERTAIN",
|
||||
False,
|
||||
),
|
||||
"MISSING_TIME": (
|
||||
(
|
||||
PurchaseOrderCandidate(
|
||||
order_no="ORDER-E",
|
||||
ordered_at="",
|
||||
payment_status="unpaid",
|
||||
),
|
||||
),
|
||||
"ORDER_MATCH_UNCERTAIN",
|
||||
True,
|
||||
),
|
||||
}
|
||||
for suffix, (candidates, error_code) in cases.items():
|
||||
for suffix, (candidates, error_code, preserve_times) in cases.items():
|
||||
with self.subTest(suffix=suffix):
|
||||
task_id = f"PUR-{suffix}"
|
||||
self._interrupt_after_irreversible(task_id)
|
||||
@@ -413,8 +441,13 @@ class PurchaseRecoveryTest(unittest.TestCase):
|
||||
"purchase"
|
||||
),
|
||||
purchase_reconcile_factory=(
|
||||
lambda _address, _cancelled, values=candidates:
|
||||
ReadOnlyReconcileAdapter(calls, values)
|
||||
lambda _address, _cancelled, values=candidates,
|
||||
keep_times=preserve_times:
|
||||
ReadOnlyReconcileAdapter(
|
||||
calls,
|
||||
values,
|
||||
preserve_times=keep_times,
|
||||
)
|
||||
),
|
||||
device_connection_checker=lambda _serial: None,
|
||||
).execute_one()
|
||||
|
||||
Reference in New Issue
Block a user