| File was renamed from talent-card-server/src/main/java/com/talentcard/server/business/payment/HuifuPaymentPayload.java |
| | |
| | | package com.talentcard.server.business.payment; |
| | | package com.talentcard.server.business.payment.huifu; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | |
| | | /** |
| | | * Shared readings of a Huifu payload. |
| | | * |
| | | * <p>A payment callback and a payment query carry the same business fields, so both are 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. |
| | | * <p>A callback and a synchronous response carry the same business fields, so payment, refund and |
| | | * withdrawal 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. |
| | | */ |
| | | final class HuifuPaymentPayload { |
| | | public final class HuifuPayload { |
| | | |
| | | private HuifuPaymentPayload() { |
| | | private HuifuPayload() { |
| | | } |
| | | |
| | | /** How the provider describes the state of a transaction. */ |
| | | enum TransStatus { |
| | | public enum TransStatus { |
| | | /** Money moved: {@code S}. */ |
| | | SUCCESS, |
| | | /** The provider declared the transaction failed or closed: {@code F}. */ |
| | |
| | | UNKNOWN |
| | | } |
| | | |
| | | static TransStatus status(String transStatus) { |
| | | public static TransStatus status(String transStatus) { |
| | | String value = text(transStatus).toUpperCase(Locale.ROOT); |
| | | return switch (value) { |
| | | case "S", "SUCCESS", "PAID" -> TransStatus.SUCCESS; |
| | |
| | | } |
| | | |
| | | /** Whether a success statement may be believed: the transaction status and the code must agree. */ |
| | | static boolean paid(String transStatus, String code) { |
| | | 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. */ |
| | | static boolean terminalFailure(String transStatus, String code) { |
| | | 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); |
| | | } |
| | | |
| | | static boolean successCode(String 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. */ |
| | | static Long amountInCent(String yuan) { |
| | | public static Long amountInCent(String yuan) { |
| | | String value = text(yuan); |
| | | if (value.isBlank()) return null; |
| | | try { |
| | |
| | | } |
| | | |
| | | /** First non-blank value of the candidate keys, looking up the provider's snake and camel spellings. */ |
| | | static String value(Map<String, Object> data, String... keys) { |
| | | 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(); |
| | |
| | | } |
| | | |
| | | /** Response code, preferring the business sub-code. */ |
| | | static String code(Map<String, Object> data) { |
| | | String code = value(data, "sub_resp_code", "resp_code", "code"); |
| | | return 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. */ |
| | | static String message(Map<String, Object> data) { |
| | | 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") |
| | | static Map<String, Object> businessData(Map<String, Object> payload) { |
| | | 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()) { |
| | |
| | | return payload; |
| | | } |
| | | |
| | | static String firstNonBlank(String first, String second) { |
| | | public static String firstNonBlank(String first, String second) { |
| | | return first == null || first.isBlank() ? (second == null ? "" : second) : first; |
| | | } |
| | | |
| | | static String text(Object value) { |
| | | public static String text(Object value) { |
| | | return value == null ? "" : String.valueOf(value).trim(); |
| | | } |
| | | } |