feat(t218): reconcile single order submissions
This commit is contained in:
+186
@@ -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
|
||||
)
|
||||
}
|
||||
+62
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user