1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package com.talentcard.server.business.payment;
 
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
 
import java.util.Map;
 
import com.talentcard.server.business.PageResult;
import com.talentcard.server.business.payment.huifu.HuifuRefundGateway;
import com.talentcard.server.business.portal.PendingUsePortalService;
import com.talentcard.server.business.portal.UsageRecordPortalService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
 
/**
 * Seam: how the talent app reads an order that was cancelled before it was ever paid.
 *
 * <p>「已取消」covers two different stories and they must not be merged: an order the talent
 * abandoned while it was still unpaid(订单取消,无需退款)and an order whose money was given back
 * (退款处理,退款中 / 退款成功 / 退款失败). The first one never involved money, so it asks the
 * provider nothing and reads as a plain cancellation; the refund dimension only adds a reason to the
 * same state, it does not take the state over.
 */
@SpringBootTest
@ActiveProfiles("test")
class CancelledOrderStateTests {
 
    @Autowired
    private JdbcTemplate jdbc;
 
    @Autowired
    private UsageRecordPortalService usageRecordPortalService;
 
    @Autowired
    private PendingUsePortalService pendingUsePortalService;
 
    @MockitoBean
    private HuifuRefundGateway refundGateway;
 
    private Long talentId;
    private String suffix;
 
    @BeforeEach
    void prepareData() {
        suffix = String.valueOf(System.nanoTime());
        jdbc.update("insert into talent_user(openid, name, mobile, status, deleted) values (?, ?, ?, 1, 0)",
                "cancelled-order-openid-" + suffix, "Cancelled order user", "13900000005");
        talentId = jdbc.queryForObject("select max(id) from talent_user", Long.class);
    }
 
    @Test
    void anOrderCancelledBeforePaymentIsStillListedAsCancelled() {
        String orderNo = cancelledUnpaidOrder();
 
        assertThat(recordOf(orderNo)).containsEntry("status", "cancelled");
    }
 
    @Test
    void anOrderCancelledBeforePaymentReadsAsACancellationThatNeedsNoRefund() {
        String orderNo = cancelledUnpaidOrder();
 
        Map<String, Object> detail = pendingUsePortalService.cancelledDetail(orderNo, talentId);
 
        assertThat(detail)
                .containsEntry("cancelSource", "订单取消")
                .containsEntry("refundStatusText", "无需退款");
        assertThat(((Number) detail.get("actualPaidAmount")).longValue())
                .as("没付过钱就不该显示退过钱")
                .isZero();
        verify(refundGateway, never()).queryRefund(any(), any());
    }
 
    /** An order the talent cancelled while it was still unpaid: no payment, no refund. */
    private String cancelledUnpaidOrder() {
        String orderNo = "CANCELLED-UNPAID-" + suffix;
        jdbc.update("""
                insert into benefit_order(order_no, talent_id, order_type, order_amount, base_pay_amount, pay_amount,
                                          order_status, verify_status, product_name_snapshot)
                values (?, ?, 'GROUP_BUY', 2000, 2000, 2000, 2, 0, '取消测试订单')
                """, orderNo, talentId);
        return orderNo;
    }
 
    private Map<String, Object> recordOf(String orderNo) {
        PageResult<Map<String, Object>> page = usageRecordPortalService.list(talentId, null, 1, 50);
        return page.records().stream()
                .filter(record -> orderNo.equals(String.valueOf(record.get("businessId"))))
                .findFirst()
                .orElseThrow(() -> new AssertionError("Order " + orderNo + " is missing from the talent order list"));
    }
}