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();
|
}
|
}
|