fix: wait for unpaid order evidence and report payable total (#325)
This commit is contained in:
+45
-6
@@ -19,7 +19,7 @@ data class FinalConfirmationEvidence(
|
||||
val activityName: String,
|
||||
)
|
||||
|
||||
data class PurchaseOrderEvidence(val orderNo: String, val submittedAt: String)
|
||||
data class PurchaseOrderEvidence(val orderNo: String, val submittedAt: String, val pddOrderAmountCent: Long? = null)
|
||||
data class PurchaseOrderReadFailure(
|
||||
val code: String,
|
||||
val message: String,
|
||||
@@ -362,7 +362,17 @@ class PurchaseLiveAutomation(
|
||||
var orderDetailEntryOpened = false
|
||||
var wechatRestorePendingSamples = 0
|
||||
var consecutiveEmptySnapshots = 0
|
||||
repeat(ORDER_RESULT_MAX_SAMPLES) { index ->
|
||||
var unpaidStartSample: Int? = null
|
||||
var unpaidSwipes = 0
|
||||
repeat(ORDER_RESULT_MAX_SAMPLES + ORDER_RESULT_UNPAID_MAX_SAMPLES) { index ->
|
||||
// The first unpaid page receives its own bounded budget, even after a long handoff.
|
||||
// Never reset it on repeated labels or navigation back to payment.
|
||||
val unpaidStart = unpaidStartSample
|
||||
if ((unpaidStart == null && index >= ORDER_RESULT_MAX_SAMPLES) ||
|
||||
(unpaidStart != null && index - unpaidStart >= ORDER_RESULT_UNPAID_MAX_SAMPLES)) {
|
||||
val failure = orderEvidenceFailure(labels)
|
||||
return unknown(failure.code, failure.message)
|
||||
}
|
||||
val snapshot = driver.capture()
|
||||
if (snapshot.packageName.isNullOrBlank()) {
|
||||
consecutiveEmptySnapshots++
|
||||
@@ -430,8 +440,16 @@ class PurchaseLiveAutomation(
|
||||
return@repeat
|
||||
}
|
||||
consecutivePaymentSamplesAfterBack = 0
|
||||
if (unpaidContextVisible) paymentPageObserved = true
|
||||
if (unpaidContextVisible) {
|
||||
paymentPageObserved = true
|
||||
if (unpaidStartSample == null) unpaidStartSample = index
|
||||
}
|
||||
if (!orderContextVisible && !unpaidContextVisible) {
|
||||
if (unpaidStartSample != null) {
|
||||
// After reaching the unpaid page only observe; never click a newly exposed control.
|
||||
pause(ORDER_RESULT_SAMPLE_INTERVAL_MS)
|
||||
return@repeat
|
||||
}
|
||||
val entries = orderDetailEntryTargets(snapshot)
|
||||
if (entries.size > 1) {
|
||||
return unknown("PURCHASE_ORDER_DETAIL_ENTRY_AMBIGUOUS", "订单详情入口不唯一,已停止只读核单")
|
||||
@@ -454,7 +472,13 @@ class PurchaseLiveAutomation(
|
||||
}
|
||||
currentLabels.forEach(labels::add)
|
||||
parseOrderEvidence(labels)?.let { return it }
|
||||
if (index > 0 && index % ORDER_RESULT_SCROLL_SAMPLE_INTERVAL == 0) {
|
||||
if (unpaidStartSample != null && unpaidSwipes < ORDER_RESULT_UNPAID_MAX_SWIPES) {
|
||||
unpaidSwipes++
|
||||
driver.swipePurchase(SwipeDirection.UP, 400)
|
||||
pause(ORDER_RESULT_UNPAID_SETTLE_MS)
|
||||
return@repeat
|
||||
}
|
||||
if (unpaidStartSample == null && index > 0 && index % ORDER_RESULT_SCROLL_SAMPLE_INTERVAL == 0) {
|
||||
driver.swipePurchase(SwipeDirection.UP, 400)
|
||||
}
|
||||
pause(ORDER_RESULT_SAMPLE_INTERVAL_MS)
|
||||
@@ -728,7 +752,18 @@ class PurchaseLiveAutomation(
|
||||
runCatching { SimpleDateFormat(pattern, Locale.ROOT).apply { isLenient = false; timeZone = TimeZone.getTimeZone("Asia/Shanghai") }.parse(normalized) }.getOrNull()
|
||||
} ?: return null
|
||||
val output = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT).apply { timeZone = TimeZone.getTimeZone("UTC") }.format(parsed)
|
||||
return PurchaseOrderEvidence(orderNumbers.single(), output)
|
||||
return PurchaseOrderEvidence(orderNumbers.single(), output, parsePayableAmount(labels))
|
||||
}
|
||||
|
||||
private fun parsePayableAmount(labels: Collection<String>): Long? {
|
||||
val amounts = labels.flatMap { label ->
|
||||
PAYABLE_AMOUNT.findAll(label).map { match ->
|
||||
runCatching { match.groupValues[1].toBigDecimal().movePointRight(2).longValueExact() }.getOrNull()
|
||||
}.toList()
|
||||
}
|
||||
// Repeated parent/child labels are fine; conflicting/overflow amounts are omitted.
|
||||
if (amounts.any { it == null }) return null
|
||||
return amounts.distinct().singleOrNull()
|
||||
}
|
||||
|
||||
private fun orderEvidenceFailure(labels: Collection<String>): PurchaseOrderReadFailure {
|
||||
@@ -764,6 +799,7 @@ class PurchaseLiveAutomation(
|
||||
val UNPAID_MARKERS = listOf("待付款", "待支付", "去支付")
|
||||
val ORDER_DETAIL_ENTRY_MARKERS = setOf("查看订单", "订单详情")
|
||||
val ORDER_CONTEXT_MARKERS = listOf("订单编号", "订单号", "下单时间", "创建时间")
|
||||
val PAYABLE_AMOUNT = Regex("(?<![\\p{L}])应付[ \\t::,,]*[¥¥]?[ \\t]*(\\d+(?:\\.\\d{1,2})?)[ \\t]*元(?![0-9.])")
|
||||
val ORDER_NO = Regex("(?:订单编号|订单号)\\s*[::]?\\s*([A-Za-z0-9-]{6,64})")
|
||||
val ORDER_TIME = Regex("(?:下单时间|创建时间)\\s*[::]?\\s*(20[0-9]{2}[-/.年][0-9]{1,2}[-/.月][0-9]{1,2}日?\\s+[0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)")
|
||||
val ANDROID_CHOOSER_PACKAGES = setOf("android", "com.android.intentresolver")
|
||||
@@ -777,7 +813,10 @@ class PurchaseLiveAutomation(
|
||||
const val ORDER_RESULT_MAX_SAMPLES = 60
|
||||
const val ORDER_RESULT_MAX_EMPTY_SAMPLES = 15
|
||||
const val ORDER_RESULT_WECHAT_RESTORE_MAX_SAMPLES = 15
|
||||
const val ORDER_RESULT_PAYMENT_POST_BACK_MAX_SAMPLES = 3
|
||||
const val ORDER_RESULT_PAYMENT_POST_BACK_MAX_SAMPLES = 25
|
||||
const val ORDER_RESULT_UNPAID_MAX_SAMPLES = 30
|
||||
const val ORDER_RESULT_UNPAID_MAX_SWIPES = 4
|
||||
const val ORDER_RESULT_UNPAID_SETTLE_MS = 500L
|
||||
const val ORDER_RESULT_SCROLL_SAMPLE_INTERVAL = 15
|
||||
const val ORDER_RESULT_SAMPLE_INTERVAL_MS = 200L
|
||||
const val SPEC_CONFIRMATION_MAX_SAMPLES = 20
|
||||
|
||||
+2
-1
@@ -79,6 +79,7 @@ data class PurchaseExecutionOutcome(
|
||||
val actualUnitPriceCent: Long? = null,
|
||||
// 仅 order_result_unknown 有意义:见 PurchaseOrderReadFailure 的说明(#302)。
|
||||
val paymentPageObserved: Boolean = false,
|
||||
val pddOrderAmountCent: Long? = null,
|
||||
)
|
||||
|
||||
class PurchaseRehearsalExecutor(
|
||||
@@ -154,7 +155,7 @@ class PurchaseRehearsalExecutor(
|
||||
}
|
||||
PurchaseActionType.READ_ORDER_RESULT -> {
|
||||
if (!irreversibleStarted) failure("PURCHASE_RULE_INVALID", "尚未进入创建订单边界")
|
||||
else live.readOrderResult()?.let { PurchaseExecutionOutcome("order_created", message = "订单已创建,等待人工检查和支付", pddOrderNo = it.orderNo, orderSubmittedAt = it.submittedAt, actualUnitPriceCent = observedPrice) }
|
||||
else live.readOrderResult()?.let { PurchaseExecutionOutcome("order_created", message = "订单已创建,等待人工检查和支付", pddOrderNo = it.orderNo, orderSubmittedAt = it.submittedAt, actualUnitPriceCent = observedPrice, pddOrderAmountCent = it.pddOrderAmountCent) }
|
||||
?: live.lastOrderReadFailure.let { readFailure ->
|
||||
PurchaseExecutionOutcome(
|
||||
"order_result_unknown",
|
||||
|
||||
@@ -724,7 +724,7 @@ class AgentForegroundService : Service() {
|
||||
paymentPageObserved = readFailure?.paymentPageObserved ?: false,
|
||||
)
|
||||
} else {
|
||||
PurchaseExecutionOutcome("order_created", message = "订单已创建,等待人工检查和支付", pddOrderNo = evidence.orderNo, orderSubmittedAt = evidence.submittedAt)
|
||||
PurchaseExecutionOutcome("order_created", message = "订单已创建,等待人工检查和支付", pddOrderNo = evidence.orderNo, orderSubmittedAt = evidence.submittedAt, pddOrderAmountCent = evidence.pddOrderAmountCent)
|
||||
}
|
||||
val requestId = UUID.randomUUID().toString()
|
||||
purchaseStore.completeAndEnqueue(interrupted.taskId, interrupted.attemptId, requestId, purchaseResultPayload(requestId, interrupted.attemptId, outcome))
|
||||
|
||||
@@ -24,5 +24,8 @@ internal fun purchaseResultPayload(
|
||||
outcome.pddOrderNo?.let { put("pddOrderNo", it) }
|
||||
outcome.orderSubmittedAt?.let { put("orderSubmittedAt", it) }
|
||||
outcome.actualUnitPriceCent?.let { put("actualUnitPriceCent", it) }
|
||||
if (outcome.resultType == "order_created") {
|
||||
outcome.pddOrderAmountCent?.let { put("pddOrderAmountCent", it) }
|
||||
}
|
||||
}
|
||||
.toString()
|
||||
|
||||
@@ -18,6 +18,104 @@ import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class PurchaseLiveAutomationTest {
|
||||
@Test
|
||||
fun `two second payment transition and two needed scrolls yield order and payable amount without clicks`() {
|
||||
val driver = ReadOnlyOrderDriver(paymentMs = 2500)
|
||||
val order = PurchaseLiveAutomation(driver, pause = { driver.elapsed += it }).readOrderResult()
|
||||
assertEquals("PDD-DEMO-325", order?.orderNo)
|
||||
assertEquals("2026-09-19T03:21:43Z", order?.submittedAt)
|
||||
assertEquals(1300L, order?.pddOrderAmountCent)
|
||||
assertEquals(1, driver.backs)
|
||||
assertEquals(2, driver.swipes)
|
||||
assertEquals(0, driver.clicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unpaid page arriving near original limit gets independent read budget`() {
|
||||
val driver = ReadOnlyOrderDriver(preludeSamples = 58)
|
||||
val order = PurchaseLiveAutomation(driver, pause = { driver.elapsed += it }).readOrderResult()
|
||||
assertEquals("PDD-DEMO-325", order?.orderNo)
|
||||
assertTrue(driver.captures > 60)
|
||||
assertEquals(2, driver.swipes)
|
||||
assertEquals(0, driver.clicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unpaid scrolling is immediate and bounded when order never becomes visible`() {
|
||||
val driver = ReadOnlyOrderDriver(requiredSwipes = 100)
|
||||
val automation = PurchaseLiveAutomation(driver, pause = { driver.elapsed += it })
|
||||
assertEquals(null, automation.readOrderResult())
|
||||
assertEquals(4, driver.swipes)
|
||||
assertEquals(0L, driver.firstSwipeAt)
|
||||
assertTrue(driver.captures <= 30)
|
||||
assertEquals(0, driver.clicks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `already visible order never scrolls and missing or conflicting payable amount never blocks order`() {
|
||||
val cases = listOf(
|
||||
emptyList<String>() to null,
|
||||
listOf("拼单价¥16", "平台优惠-¥3", "实付:13元") to null,
|
||||
listOf("应付:13元", "应付:14元") to null,
|
||||
listOf("应付:-13元", "应付:13.123元", "预计应付:13元") to null,
|
||||
listOf("应付:999999999999999999999元") to null,
|
||||
listOf("应付:13.20元", "应付:13.20元") to 1320L,
|
||||
listOf("应付:0元") to 0L,
|
||||
)
|
||||
for ((amounts, expected) in cases) {
|
||||
val driver = ReadOnlyOrderDriver(requiredSwipes = 0, amounts = amounts)
|
||||
val order = PurchaseLiveAutomation(driver, pause = {}).readOrderResult()
|
||||
assertEquals("PDD-DEMO-325", order?.orderNo)
|
||||
assertEquals(expected, order?.pddOrderAmountCent)
|
||||
assertEquals(0, driver.swipes)
|
||||
assertEquals(0, driver.clicks)
|
||||
}
|
||||
}
|
||||
|
||||
private class ReadOnlyOrderDriver(
|
||||
val paymentMs: Long = 0,
|
||||
val preludeSamples: Int = 0,
|
||||
val requiredSwipes: Int = 2,
|
||||
val amounts: List<String> = listOf("拼单价¥16", "平台优惠-¥3", "已优惠3元使用3元平台无门槛券,应付:,13元,(免运费)"),
|
||||
) : PurchaseUiDriver by LiveDriver() {
|
||||
var elapsed = 0L
|
||||
var captures = 0
|
||||
var backs = 0
|
||||
var swipes = 0
|
||||
var clicks = 0
|
||||
var firstSwipeAt: Long? = null
|
||||
override fun capture(): UiSnapshot {
|
||||
captures++
|
||||
if (captures <= preludeSamples) return UiSnapshot(PDD, ACTIVITY, emptyList())
|
||||
if (elapsed < paymentMs) return UiSnapshot(PDD, "com.xunmeng.pinduoduo.app_pay.core.PayActivity", listOf(node("立即支付")))
|
||||
return UiSnapshot(PDD, ACTIVITY, buildList {
|
||||
add(node("待付款"))
|
||||
// Amount scrolls away before the order fields become visible.
|
||||
if (swipes == 0) amounts.forEach { add(node(it)) }
|
||||
add(node("订单号:PDD-DEMO-325", swipes >= requiredSwipes))
|
||||
add(node("下单时间:2026-09-19 11:21:43", swipes >= requiredSwipes))
|
||||
add(node("去支付"))
|
||||
})
|
||||
}
|
||||
override fun backPurchase(): Boolean { backs++; return true }
|
||||
override fun swipePurchase(direction: SwipeDirection, durationMs: Long): Boolean {
|
||||
assertEquals(SwipeDirection.UP, direction)
|
||||
assertTrue(elapsed >= paymentMs)
|
||||
if (firstSwipeAt == null) firstSwipeAt = elapsed
|
||||
swipes++
|
||||
return true
|
||||
}
|
||||
override fun clickFresh(target: SnapshotNode): FreshActionResult {
|
||||
clicks++
|
||||
throw AssertionError("Read-only unpaid branch must never click")
|
||||
}
|
||||
private fun node(text: String, visible: Boolean = true) = SnapshotNode(
|
||||
text, null, text, null, null, "android.widget.TextView",
|
||||
NodeBounds(0, 100, 500, if (visible) 180 else 100),
|
||||
false, false, false, false, true, visible,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verified spec panel advances through one exact confirm target`() {
|
||||
val driver = SpecConfirmationDriver()
|
||||
@@ -358,10 +456,10 @@ class PurchaseLiveAutomationTest {
|
||||
assertEquals("PURCHASE_ORDER_PAYMENT_REPEATED", automation.lastOrderReadFailure?.code)
|
||||
assertEquals(
|
||||
"支付页安全返回后持续无订单证据,已停止自动核单" +
|
||||
"[paymentBackAttempts=1;consecutivePaymentSamplesAfterBack=3]",
|
||||
"[paymentBackAttempts=1;consecutivePaymentSamplesAfterBack=25]",
|
||||
automation.lastOrderReadFailure?.message,
|
||||
)
|
||||
assertEquals(4, driver.postSubmitCaptureCount)
|
||||
assertEquals(26, driver.postSubmitCaptureCount)
|
||||
assertEquals(1, driver.postSubmitBackCount)
|
||||
assertEquals(0, driver.genericSwipes)
|
||||
assertFalse(driver.clicked.any { it.contains("支付") })
|
||||
|
||||
@@ -8,6 +8,16 @@ import org.junit.Assert.assertFalse
|
||||
import org.junit.Test
|
||||
|
||||
class PurchaseResultPayloadTest {
|
||||
@Test
|
||||
fun `created order serializes optional payable total separately from unit price`() {
|
||||
val outcome = PurchaseExecutionOutcome("order_created", message = "test", actualUnitPriceCent = 1600, pddOrderAmountCent = 1300)
|
||||
val payload = JSONObject(purchaseResultPayload("r", "a", outcome))
|
||||
assertEquals(1300L, payload.getLong("pddOrderAmountCent"))
|
||||
assertEquals(1600L, payload.getLong("actualUnitPriceCent"))
|
||||
assertFalse(JSONObject(purchaseResultPayload("r", "a", outcome.copy(pddOrderAmountCent = null))).has("pddOrderAmountCent"))
|
||||
assertFalse(JSONObject(purchaseResultPayload("r", "a", outcome.copy(resultType = "order_result_unknown"))).has("pddOrderAmountCent"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown order result keeps stable failure and verified price without order fields`() {
|
||||
val payload = JSONObject(purchaseResultPayload(
|
||||
|
||||
@@ -341,6 +341,9 @@ func (s *Service) SubmitResult(ctx context.Context, taskID uint64, req ResultReq
|
||||
if req.ActualUnitPriceCent != nil && *req.ActualUnitPriceCent < 0 {
|
||||
return TaskPayload{}, fail(CodeInvalidRequest, "实际单价无效")
|
||||
}
|
||||
if req.PDDOrderAmountCent != nil && (*req.PDDOrderAmountCent < 0 || req.ResultType != "order_created") {
|
||||
return TaskPayload{}, fail(CodeInvalidRequest, "订单应付金额仅可随已创建订单提交且不能为负数")
|
||||
}
|
||||
now := s.Now()
|
||||
next := ""
|
||||
switch req.ResultType {
|
||||
@@ -396,6 +399,9 @@ func (s *Service) SubmitResult(ctx context.Context, taskID uint64, req ResultReq
|
||||
a.Status = models.PurchaseAttemptStatusFailed
|
||||
a.ErrorCode, a.ErrorMessage = &failureCode, &message
|
||||
}
|
||||
if next == models.PurchaseTaskStatusOrderCreated && req.PDDOrderAmountCent != nil && t.PDDOrderAmountCent == nil {
|
||||
t.PDDOrderAmountCent = req.PDDOrderAmountCent
|
||||
}
|
||||
case "order_result_unknown":
|
||||
if t.ExecutionMode != models.PurchaseExecutionModeLive || t.Status != models.PurchaseTaskStatusOrderSubmitStarted {
|
||||
return TaskPayload{}, fail(CodeStateConflict, "当前任务不能标记订单结果未知")
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package purchase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go-admin/app/goauto/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestOrderResultPayableAmount(t *testing.T) {
|
||||
ptr := func(n int64) *int64 { return &n }
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
amount, existing, want *int64
|
||||
result string
|
||||
invalid bool
|
||||
}{
|
||||
{"payable distinct from unit price", ptr(1300), nil, ptr(1300), "order_created", false},
|
||||
{"zero", ptr(0), nil, ptr(0), "order_created", false},
|
||||
{"old agent omitted", nil, nil, nil, "order_created", false},
|
||||
{"omitted preserves existing", nil, ptr(1200), ptr(1200), "order_created", false},
|
||||
{"does not overwrite existing", ptr(1300), ptr(1200), ptr(1200), "order_created", false},
|
||||
{"negative", ptr(-1), nil, nil, "order_created", true},
|
||||
{"unknown cannot attach amount", ptr(1300), nil, nil, "order_result_unknown", true},
|
||||
{"failed cannot attach amount", ptr(1300), nil, nil, "failed", true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
db := testDB(t)
|
||||
f := seed(t, db, liveCaps(), true)
|
||||
s := testService(db)
|
||||
task, err := createLive(t, s, f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
started := startLivePurchaseAfterProbe(t, s, f, task)
|
||||
if _, err = s.MarkOrderSubmitStarted(context.Background(), task.ID, ActionRequest{RequestID: uuid.NewString()}, f.token); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tc.existing != nil {
|
||||
if err = db.Session(&gorm.Session{SkipHooks: true}).Model(&models.PurchaseTask{}).Where("id = ?", task.ID).Update("pdd_order_amount_cent", *tc.existing).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
now := s.Now()
|
||||
req := ResultRequest{RequestID: uuid.NewString(), TaskAttemptID: started.TaskAttemptID,
|
||||
ResultType: tc.result, PDDOrderNo: "PDD-DEMO-325", OrderSubmittedAt: &now,
|
||||
ActualUnitPriceCent: ptr(1600), PDDOrderAmountCent: tc.amount}
|
||||
_, err = s.SubmitResult(context.Background(), task.ID, req, f.token)
|
||||
if tc.invalid {
|
||||
if code(err) != CodeInvalidRequest {
|
||||
t.Fatalf("want invalid request, got %v", err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
replay, e := s.SubmitResult(context.Background(), task.ID, req, f.token)
|
||||
if e != nil || !replay.Replayed {
|
||||
t.Fatalf("replay failed: %v", e)
|
||||
}
|
||||
req.PDDOrderAmountCent = ptr(999)
|
||||
if _, e = s.SubmitResult(context.Background(), task.ID, req, f.token); code(e) != CodeResultConflict {
|
||||
t.Fatalf("changed amount replay accepted: %v", e)
|
||||
}
|
||||
queued := loadOrderWriteback(t, db, task.ID)
|
||||
if queued.OrderNo != "PDD-DEMO-325" {
|
||||
t.Fatal("SYB order number queue missing")
|
||||
}
|
||||
}
|
||||
var saved models.PurchaseTask
|
||||
if e := db.First(&saved, task.ID).Error; e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
if (saved.PDDOrderAmountCent == nil) != (tc.want == nil) || (tc.want != nil && *saved.PDDOrderAmountCent != *tc.want) {
|
||||
t.Fatal("unexpected saved payable amount")
|
||||
}
|
||||
if tc.invalid {
|
||||
if saved.Status != models.PurchaseTaskStatusOrderSubmitStarted {
|
||||
t.Fatal("invalid result changed status")
|
||||
}
|
||||
} else if saved.ActualUnitPriceCent == nil || *saved.ActualUnitPriceCent != 1600 {
|
||||
t.Fatal("unit price changed")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -157,6 +157,7 @@ type ResultRequest struct {
|
||||
PDDOrderNo string `json:"pddOrderNo,omitempty"`
|
||||
OrderSubmittedAt *time.Time `json:"orderSubmittedAt,omitempty"`
|
||||
ActualUnitPriceCent *int64 `json:"actualUnitPriceCent,omitempty"`
|
||||
PDDOrderAmountCent *int64 `json:"pddOrderAmountCent,omitempty"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
ErrorMessage string `json:"errorMessage,omitempty"`
|
||||
ProbedSpecs json.RawMessage `json:"probedSpecs,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user