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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package com.talentcard.server.business.payment.huifu;
 
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Locale;
import java.util.Map;
 
import com.alibaba.fastjson.JSON;
 
/**
 * Shared readings of a Huifu payload.
 *
 * <p>A callback and a synchronous response carry the same business fields, so payment, delay
 * confirmation and refund results are all interpreted here instead of each caller keeping its own
 * field names, money conversion and status rules. The provider reports money in yuan with two
 * decimals while local amounts are integer cents.
 */
public final class HuifuPayload {
 
    private HuifuPayload() {
    }
 
    /** How the provider describes the state of a transaction. */
    public enum TransStatus {
        /** Money moved: {@code S}. */
        SUCCESS,
        /** The provider declared the transaction failed or closed: {@code F}. */
        FAILED,
        /** Still in flight: {@code P}. */
        PENDING,
        /** A status this integration does not know, including a missing one. */
        UNKNOWN
    }
 
    public static TransStatus status(String transStatus) {
        String value = text(transStatus).toUpperCase(Locale.ROOT);
        return switch (value) {
            case "S", "SUCCESS", "PAID" -> TransStatus.SUCCESS;
            case "F", "FAILED" -> TransStatus.FAILED;
            case "P", "PROCESSING", "PENDING" -> TransStatus.PENDING;
            default -> TransStatus.UNKNOWN;
        };
    }
 
    /** Whether a success statement may be believed: the transaction status and the code must agree. */
    public static boolean paid(String transStatus, String code) {
        return status(transStatus) == TransStatus.SUCCESS && successCode(code);
    }
 
    /** A notification that will never settle by itself, used to close refunds. */
    public static boolean terminalFailure(String transStatus, String code) {
        TransStatus status = status(transStatus);
        if (status == TransStatus.PENDING) return false;
        if (status == TransStatus.FAILED) return true;
        return !successCode(code);
    }
 
    public static boolean successCode(String code) {
        String value = text(code);
        return "00000000".equals(value) || "10000".equals(value) || "0".equals(value) || "SUCCESS".equalsIgnoreCase(value);
    }
 
    /** Provider amount in yuan with two decimals, converted to integer cents. Null when unusable. */
    public static Long amountInCent(String yuan) {
        String value = text(yuan);
        if (value.isBlank()) return null;
        try {
            return new BigDecimal(value).movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValueExact();
        } catch (RuntimeException exception) {
            return null;
        }
    }
 
    /** First non-blank value of the candidate keys, looking up the provider's snake and camel spellings. */
    public static String value(Map<String, Object> data, String... keys) {
        for (String key : keys) {
            Object value = data.get(key);
            if (value != null && !String.valueOf(value).isBlank()) return String.valueOf(value).trim();
        }
        return "";
    }
 
    /** Response code, preferring the business sub-code. */
    public static String code(Map<String, Object> data) {
        return value(data, "sub_resp_code", "resp_code", "code");
    }
 
    /** Response message, preferring the business sub-description. */
    public static String message(Map<String, Object> data) {
        return value(data, "sub_resp_desc", "resp_desc", "message");
    }
 
    /** Huifu may nest the business fields under {@code data}, as an object or as a JSON string. */
    @SuppressWarnings("unchecked")
    public static Map<String, Object> businessData(Map<String, Object> payload) {
        Object nested = payload.get("data");
        if (nested instanceof Map<?, ?> value) return (Map<String, Object>) value;
        if (nested instanceof String value && !value.isBlank()) {
            try {
                return JSON.parseObject(value, Map.class);
            } catch (Exception ignored) {
                return payload;
            }
        }
        return payload;
    }
 
    public static String firstNonBlank(String first, String second) {
        return first == null || first.isBlank() ? (second == null ? "" : second) : first;
    }
 
    public static String text(Object value) {
        return value == null ? "" : String.valueOf(value).trim();
    }
}