feat(t218): reconcile single order submissions

This commit is contained in:
QiuSW
2026-07-28 17:48:54 +08:00
parent 682cc7327c
commit a77d8be06d
32 changed files with 4306 additions and 79 deletions
@@ -0,0 +1,186 @@
package com.roubao.autopilot.pinduoduo
import java.time.Instant
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class PinduoduoOrderSubmissionModelsTest {
private val expectation = PinduoduoOrderSubmissionExpectation(
title = "设计拼接T恤短袖2026轻奢小众夏季气质新款百搭漂亮上衣出片",
color = "GRAY",
size = "2XL",
quantity = 1,
totalPriceCents = 1_435L
)
@Test
fun `safe confirmation requires address and one exact submit action`() {
val readiness = PinduoduoOrderSubmissionSafetyPolicy.inspect(
confirmationElements(),
expectation
)
assertNotNull(readiness)
assertTrue(readiness!!.addressConfigured)
assertEquals(1, readiness.exactSubmitActionCount)
}
@Test
fun `missing address and automatic payment semantics fail closed`() {
assertNull(
PinduoduoOrderSubmissionSafetyPolicy.inspect(
confirmationElements().map {
if (it.text == "张三 138****1234") {
it.copy(text = "手动添加收货地址")
} else {
it
}
},
expectation
)
)
assertNull(
PinduoduoOrderSubmissionSafetyPolicy.inspect(
confirmationElements() + element("先用后付"),
expectation
)
)
}
@Test
fun `duplicate or payment submit actions are never accepted`() {
assertNull(
PinduoduoOrderSubmissionSafetyPolicy.inspect(
confirmationElements() + element(
"提交订单",
clickable = true
),
expectation
)
)
assertNull(
PinduoduoOrderSubmissionSafetyPolicy.inspect(
confirmationElements().map {
if (it.text == "提交订单") {
it.copy(text = "立即支付")
} else {
it
}
},
expectation
)
)
}
@Test
fun `pending order parser reads labeled identity and local order time`() {
val parsed = PinduoduoPendingOrderParser.parse(orderElements())
assertNotNull(parsed)
assertEquals("12345678901234567890", parsed!!.platformOrderNo)
assertEquals("PENDING_PAYMENT", parsed.status)
assertEquals("2026-07-28T01:31:00Z", parsed.platformOrderedAt)
assertEquals(1_435L, parsed.totalPriceCents)
}
@Test
fun `reconciliation requires one exact recent pending order`() {
val parsed = requireNotNull(
PinduoduoPendingOrderParser.parse(orderElements())
)
val matched = PinduoduoOrderReconciliationPolicy.reconcile(
listOf(parsed),
expectation,
fencedAt = "2026-07-28T01:30:00Z",
now = Instant.parse("2026-07-28T01:32:00Z")
)
assertTrue(matched is PinduoduoOrderReconciliation.Matched)
val ambiguous = PinduoduoOrderReconciliationPolicy.reconcile(
listOf(
parsed,
parsed.copy(
platformOrderNo = "22345678901234567890"
)
),
expectation,
fencedAt = "2026-07-28T01:30:00Z",
now = Instant.parse("2026-07-28T01:32:00Z")
)
assertEquals(
"ORDER_AMBIGUOUS",
(ambiguous as PinduoduoOrderReconciliation.ManualReview)
.reasonCode
)
}
@Test
fun `unlabeled order numbers and stale times fail closed`() {
assertNull(
PinduoduoPendingOrderParser.parse(
orderElements().map {
if (it.text?.startsWith("订单编号") == true) {
it.copy(text = "12345678901234567890")
} else {
it
}
}
)
)
val parsed = requireNotNull(
PinduoduoPendingOrderParser.parse(orderElements())
)
val result = PinduoduoOrderReconciliationPolicy.reconcile(
listOf(
parsed.copy(
platformOrderedAt = "2026-07-27T01:31:00Z"
)
),
expectation,
fencedAt = "2026-07-28T01:30:00Z",
now = Instant.parse("2026-07-28T01:32:00Z")
)
assertEquals(
"ORDER_NOT_FOUND",
(result as PinduoduoOrderReconciliation.ManualReview)
.reasonCode
)
}
private fun confirmationElements() = listOf(
element("收货地址"),
element("张三 138****1234"),
element(expectation.title),
element("已选:灰色,2XL 建议125-135斤"),
element("×1"),
element("实付款¥14.35"),
element("提交订单", clickable = true)
)
private fun orderElements() = listOf(
element("待付款"),
element(expectation.title),
element("规格:灰色,2XL 建议125-135斤"),
element("×1"),
element("应付¥14.35"),
element("订单编号:12345678901234567890"),
element("下单时间:2026-07-28 09:31")
)
private fun element(
text: String,
clickable: Boolean = false
) = PinduoduoUiElement(
text = text,
contentDescription = null,
className = "android.widget.TextView",
resourceId = null,
clickable = clickable,
editable = false,
enabled = true,
visibleToUser = true
)
}
@@ -0,0 +1,62 @@
package com.roubao.autopilot.procurement
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class OrderSubmissionRecoveryPolicyTest {
@Test
fun `fresh fence response permits only the current call to click`() {
val transition = OrderSubmissionRecoveryPolicy.afterFenceResponse(
OrderSubmissionStatus.FENCE_INTENT_SAVED,
"FENCED",
intentCreatedInCurrentCall = true
)
assertEquals(OrderSubmissionStatus.CLICK_ASSUMED, transition.status)
assertTrue(transition.mayClickInCurrentCall)
assertFalse(
OrderSubmissionRecoveryPolicy.mayResumeClick(transition.status)
)
}
@Test
fun `persisted fence intent can never regain click permission`() {
val transition = OrderSubmissionRecoveryPolicy.afterFenceResponse(
OrderSubmissionStatus.FENCE_INTENT_SAVED,
"FENCED",
intentCreatedInCurrentCall = false
)
assertEquals(OrderSubmissionStatus.CLICK_ASSUMED, transition.status)
assertFalse(transition.mayClickInCurrentCall)
}
@Test
fun `persisted click assumed can never regain click permission`() {
val transition = OrderSubmissionRecoveryPolicy.afterFenceResponse(
OrderSubmissionStatus.CLICK_ASSUMED,
"FENCED",
intentCreatedInCurrentCall = false
)
assertEquals(OrderSubmissionStatus.CLICK_ASSUMED, transition.status)
assertFalse(transition.mayClickInCurrentCall)
OrderSubmissionStatus.entries.forEach { status ->
assertFalse(OrderSubmissionRecoveryPolicy.mayResumeClick(status))
}
}
@Test
fun `terminal backend states never permit a click`() {
listOf("MANUAL_REVIEW", "RECONCILED").forEach { remote ->
val transition = OrderSubmissionRecoveryPolicy.afterFenceResponse(
OrderSubmissionStatus.FENCE_INTENT_SAVED,
remote,
intentCreatedInCurrentCall = true
)
assertFalse(transition.mayClickInCurrentCall)
}
}
}