|
@@ -0,0 +1,531 @@
|
|
|
|
|
+package com.ruoyi.app.pay;
|
|
|
|
|
+
|
|
|
|
|
+import com.alibaba.fastjson2.JSONArray;
|
|
|
|
|
+import com.alibaba.fastjson2.JSONObject;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
|
|
+import com.ruoyi.app.pay.dto.LinePayOfflineResult;
|
|
|
|
|
+import com.ruoyi.app.utils.linepay.LinePayClient;
|
|
|
|
|
+import com.ruoyi.app.utils.linepay.LinePayCredential;
|
|
|
|
|
+import com.ruoyi.app.utils.linepay.LinePayOfflineRequest;
|
|
|
|
|
+import com.ruoyi.app.utils.linepay.LinePayResponse;
|
|
|
|
|
+import com.ruoyi.common.exception.ServiceException;
|
|
|
|
|
+import com.ruoyi.common.utils.MessageUtils;
|
|
|
|
|
+import com.ruoyi.system.domain.InfoUser;
|
|
|
|
|
+import com.ruoyi.system.domain.PosOrder;
|
|
|
|
|
+import com.ruoyi.system.domain.PosOrderLinePayment;
|
|
|
|
|
+import com.ruoyi.system.domain.PosStore;
|
|
|
|
|
+import com.ruoyi.system.domain.PosStoreLinePay;
|
|
|
|
|
+import com.ruoyi.system.service.IInfoUserService;
|
|
|
|
|
+import com.ruoyi.system.service.IPosOrderLinePaymentService;
|
|
|
|
|
+import com.ruoyi.system.service.IPosOrderService;
|
|
|
|
|
+import com.ruoyi.system.service.IPosStoreService;
|
|
|
|
|
+import com.ruoyi.system.service.IPosStoreLinePayService;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.dao.DuplicateKeyException;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.Comparator;
|
|
|
|
|
+import java.util.Date;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Locale;
|
|
|
|
|
+import java.util.Set;
|
|
|
|
|
+import java.util.UUID;
|
|
|
|
|
+
|
|
|
|
|
+/** Merchant-presented LINE Pay Offline My Code orchestration. */
|
|
|
|
|
+@Service
|
|
|
|
|
+public class LinePayOfflineService {
|
|
|
|
|
+ private static final String MODE_OFFLINE = "OFFLINE";
|
|
|
|
|
+ private static final Set<String> AUTH_REQUIRED_CODES = Set.of("1145", "1169");
|
|
|
|
|
+ private static final Set<String> UNCERTAIN_CODES = Set.of(
|
|
|
|
|
+ "1152", "1172", "1198", "1199", "9000");
|
|
|
|
|
+ private static final Set<String> DEFINITE_PAY_FAILURE_CODES = Set.of(
|
|
|
|
|
+ "1101", "1102", "1104", "1105", "1106", "1110", "1124", "1133",
|
|
|
|
|
+ "1141", "1142");
|
|
|
|
|
+
|
|
|
|
|
+ @Value("${line-pay.reconcile.auth-deadline-minutes:30}")
|
|
|
|
|
+ private long authDeadlineMinutes = 30L;
|
|
|
|
|
+ @Value("${line-pay.reconcile.unknown-deadline-hours:24}")
|
|
|
|
|
+ private long unknownDeadlineHours = 24L;
|
|
|
|
|
+
|
|
|
|
|
+ private final IPosOrderService orderService;
|
|
|
|
|
+ private final IInfoUserService userService;
|
|
|
|
|
+ private final IPosStoreService storeService;
|
|
|
|
|
+ private final IPosStoreLinePayService credentialService;
|
|
|
|
|
+ private final IPosOrderLinePaymentService paymentService;
|
|
|
|
|
+ private final LinePayClient linePayClient;
|
|
|
|
|
+ private final LinePayFactService factService;
|
|
|
|
|
+ private final LinePayGatewayAuditService auditService;
|
|
|
|
|
+ private final PaymentCreateGuardService createGuard;
|
|
|
|
|
+
|
|
|
|
|
+ public LinePayOfflineService(IPosOrderService orderService,
|
|
|
|
|
+ IInfoUserService userService,
|
|
|
|
|
+ IPosStoreService storeService,
|
|
|
|
|
+ IPosStoreLinePayService credentialService,
|
|
|
|
|
+ IPosOrderLinePaymentService paymentService,
|
|
|
|
|
+ LinePayClient linePayClient,
|
|
|
|
|
+ LinePayFactService factService) {
|
|
|
|
|
+ this(orderService, userService, storeService, credentialService, paymentService, linePayClient,
|
|
|
|
|
+ factService, null, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ public LinePayOfflineService(IPosOrderService orderService,
|
|
|
|
|
+ IInfoUserService userService,
|
|
|
|
|
+ IPosStoreService storeService,
|
|
|
|
|
+ IPosStoreLinePayService credentialService,
|
|
|
|
|
+ IPosOrderLinePaymentService paymentService,
|
|
|
|
|
+ LinePayClient linePayClient,
|
|
|
|
|
+ LinePayFactService factService,
|
|
|
|
|
+ LinePayGatewayAuditService auditService,
|
|
|
|
|
+ PaymentCreateGuardService createGuard) {
|
|
|
|
|
+ this.orderService = orderService;
|
|
|
|
|
+ this.userService = userService;
|
|
|
|
|
+ this.storeService = storeService;
|
|
|
|
|
+ this.credentialService = credentialService;
|
|
|
|
|
+ this.paymentService = paymentService;
|
|
|
|
|
+ this.linePayClient = linePayClient;
|
|
|
|
|
+ this.factService = factService;
|
|
|
|
|
+ this.auditService = auditService;
|
|
|
|
|
+ this.createGuard = createGuard;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public LinePayOfflineResult pay(Long merchantId, String ddId, String oneTimeKey) {
|
|
|
|
|
+ if (oneTimeKey == null || !oneTimeKey.matches("\\d{18}")) {
|
|
|
|
|
+ throw new ServiceException(MessageUtils.message("line.pay.offline.mycode.invalid"));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (createGuard == null) {
|
|
|
|
|
+ return payUnderLock(merchantId, normalizeDdId(ddId), oneTimeKey);
|
|
|
|
|
+ }
|
|
|
|
|
+ String normalized = normalizeDdId(ddId);
|
|
|
|
|
+ return createGuard.withLock(normalized,
|
|
|
|
|
+ () -> payUnderLock(merchantId, normalized, oneTimeKey));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private LinePayOfflineResult payUnderLock(Long merchantId, String ddId, String oneTimeKey) {
|
|
|
|
|
+ PosOrder order = requireMerchantOrder(merchantId, ddId, true);
|
|
|
|
|
+ PosOrderLinePayment active = paymentService.getActiveByDdId(ddId);
|
|
|
|
|
+ if (active != null) {
|
|
|
|
|
+ if (!MODE_OFFLINE.equals(active.getPaymentMode())) {
|
|
|
|
|
+ throw new ServiceException(MessageUtils.message("line.pay.offline.attempt.blocked"));
|
|
|
|
|
+ }
|
|
|
|
|
+ return result(active, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ PosStoreLinePay credential = credentialService.getEnabledCurrent(order.getMdId());
|
|
|
|
|
+ if (credential == null) {
|
|
|
|
|
+ throw new ServiceException(MessageUtils.message("line.pay.offline.not.enabled"));
|
|
|
|
|
+ }
|
|
|
|
|
+ IntentSelection selection = createIntent(order, credential);
|
|
|
|
|
+ PosOrderLinePayment intent = selection.payment();
|
|
|
|
|
+ if (!selection.created() || !"REQUESTING".equals(intent.getStatus())) {
|
|
|
|
|
+ return result(intent, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ LinePayCredential gatewayCredential = toCredential(credential);
|
|
|
|
|
+ try {
|
|
|
|
|
+ LinePayOfflineRequest request = new LinePayOfflineRequest(intent.getLineOrderId(),
|
|
|
|
|
+ ddId, "Order " + ddId, intent.getAmount(), intent.getCurrency(), oneTimeKey);
|
|
|
|
|
+ LinePayResponse response = auditService == null
|
|
|
|
|
+ ? linePayClient.payOffline(gatewayCredential, request)
|
|
|
|
|
+ : auditService.executeOffline("APP", intent.getId(), credential.getId(),
|
|
|
|
|
+ intent.getStoreId(), intent.getDdId(), intent.getLineOrderId(),
|
|
|
|
|
+ () -> linePayClient.payOffline(gatewayCredential, request));
|
|
|
|
|
+ return handlePayResponse(intent, response);
|
|
|
|
|
+ } catch (Exception unknown) {
|
|
|
|
|
+ int changed = paymentService.markRequestUnknown(intent.getId(), intent.getVersion(),
|
|
|
|
|
+ shortly(), unknownDeadline());
|
|
|
|
|
+ if (changed != 1) {
|
|
|
|
|
+ return result(reload(intent), false);
|
|
|
|
|
+ }
|
|
|
|
|
+ intent.setStatus("REQUEST_UNKNOWN");
|
|
|
|
|
+ intent.setUpdateTime(new Date());
|
|
|
|
|
+ return result(intent, false);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public LinePayOfflineResult status(Long merchantId, String ddId) {
|
|
|
|
|
+ PosOrder order = requireMerchantOrder(merchantId, normalizeDdId(ddId), false);
|
|
|
|
|
+ PosOrderLinePayment selected = paymentService.getByDdId(String.valueOf(order.getDdId())).stream()
|
|
|
|
|
+ .filter(payment -> MODE_OFFLINE.equals(payment.getPaymentMode()))
|
|
|
|
|
+ .max(Comparator.comparing(PosOrderLinePayment::getCreateTime,
|
|
|
|
|
+ Comparator.nullsFirst(Date::compareTo))
|
|
|
|
|
+ .thenComparing(PosOrderLinePayment::getId,
|
|
|
|
|
+ Comparator.nullsFirst(Long::compareTo)))
|
|
|
|
|
+ .orElseThrow(() -> new ServiceException(
|
|
|
|
|
+ MessageUtils.message("line.pay.offline.attempt.not.found")));
|
|
|
|
|
+ return result(selected, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public String reconcilePayment(Long paymentId, String source) {
|
|
|
|
|
+ PosOrderLinePayment payment = paymentService.getById(paymentId);
|
|
|
|
|
+ if (payment == null || !MODE_OFFLINE.equals(payment.getPaymentMode())) {
|
|
|
|
|
+ throw new ServiceException(MessageUtils.message("line.pay.offline.attempt.not.found"));
|
|
|
|
|
+ }
|
|
|
|
|
+ PosStoreLinePay credential = credentialService.getById(payment.getCredentialId());
|
|
|
|
|
+ if (credential == null) {
|
|
|
|
|
+ int changed = paymentService.markManualReview(
|
|
|
|
|
+ payment.getId(), payment.getVersion(), payment.getStatus());
|
|
|
|
|
+ return changed == 1 ? "MANUAL_REVIEW" : reload(payment).getStatus();
|
|
|
|
|
+ }
|
|
|
|
|
+ boolean expired = payment.getReconcileDeadline() != null
|
|
|
|
|
+ && !new Date().before(payment.getReconcileDeadline());
|
|
|
|
|
+ try {
|
|
|
|
|
+ LinePayCredential gatewayCredential = toCredential(credential);
|
|
|
|
|
+ LinePayResponse response = auditService == null
|
|
|
|
|
+ ? linePayClient.checkOffline(gatewayCredential, payment.getLineOrderId())
|
|
|
|
|
+ : auditService.execute("CHECK", source == null ? "TASK" : source,
|
|
|
|
|
+ payment.getId(), null, credential.getId(), payment.getStoreId(),
|
|
|
|
|
+ payment.getDdId(), payment.getLineOrderId(), payment.getTransactionId(),
|
|
|
|
|
+ () -> linePayClient.checkOffline(gatewayCredential, payment.getLineOrderId()));
|
|
|
|
|
+ return handleCheckResponse(payment, response, expired);
|
|
|
|
|
+ } catch (Exception unknown) {
|
|
|
|
|
+ if (expired) {
|
|
|
|
|
+ int changed = paymentService.markManualReview(
|
|
|
|
|
+ payment.getId(), payment.getVersion(), payment.getStatus());
|
|
|
|
|
+ return changed == 1 ? "MANUAL_REVIEW" : reload(payment).getStatus();
|
|
|
|
|
+ }
|
|
|
|
|
+ int changed = paymentService.rescheduleOffline(
|
|
|
|
|
+ payment.getId(), payment.getVersion(), payment.getStatus(),
|
|
|
|
|
+ shortly(), payment.getReconcileDeadline() == null
|
|
|
|
|
+ ? unknownDeadline() : payment.getReconcileDeadline());
|
|
|
|
|
+ return changed == 1 ? payment.getStatus() : reload(payment).getStatus();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private LinePayOfflineResult handlePayResponse(PosOrderLinePayment payment,
|
|
|
|
|
+ LinePayResponse response) {
|
|
|
|
|
+ if (response != null && AUTH_REQUIRED_CODES.contains(response.returnCode())) {
|
|
|
|
|
+ int changed = paymentService.markOfflineWaitingAuth(payment.getId(), payment.getVersion(),
|
|
|
|
|
+ response.transactionId(), shortly(), waitingAuthDeadline(payment));
|
|
|
|
|
+ if (changed != 1) {
|
|
|
|
|
+ return result(reload(payment), false);
|
|
|
|
|
+ }
|
|
|
|
|
+ payment.setTransactionId(response.transactionId());
|
|
|
|
|
+ payment.setStatus("WAITING_AUTH");
|
|
|
|
|
+ payment.setUpdateTime(new Date());
|
|
|
|
|
+ return result(payment, false);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (response != null && response.isSuccess()) {
|
|
|
|
|
+ return applyCompletedFact(payment, response);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (isDefinitePayFailure(response)) {
|
|
|
|
|
+ int changed = paymentService.markTerminal(payment.getId(), payment.getVersion(),
|
|
|
|
|
+ payment.getStatus(), "FAILED");
|
|
|
|
|
+ if (changed != 1) {
|
|
|
|
|
+ return result(reload(payment), false);
|
|
|
|
|
+ }
|
|
|
|
|
+ payment.setStatus("FAILED");
|
|
|
|
|
+ payment.setActiveDdId(null);
|
|
|
|
|
+ payment.setUpdateTime(new Date());
|
|
|
|
|
+ return result(payment, false);
|
|
|
|
|
+ }
|
|
|
|
|
+ int changed = paymentService.markRequestUnknown(payment.getId(), payment.getVersion(),
|
|
|
|
|
+ shortly(), unknownDeadline());
|
|
|
|
|
+ if (changed != 1) {
|
|
|
|
|
+ return result(reload(payment), false);
|
|
|
|
|
+ }
|
|
|
|
|
+ payment.setStatus("REQUEST_UNKNOWN");
|
|
|
|
|
+ payment.setUpdateTime(new Date());
|
|
|
|
|
+ return result(payment, false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String handleCheckResponse(PosOrderLinePayment payment, LinePayResponse response,
|
|
|
|
|
+ boolean expired) {
|
|
|
|
|
+ if (response == null || !response.isSuccess()) {
|
|
|
|
|
+ return rescheduleOrReview(payment, expired);
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONObject info = response.info();
|
|
|
|
|
+ String gatewayStatus = info == null ? null : info.getString("status");
|
|
|
|
|
+ if (gatewayStatus == null && info != null) {
|
|
|
|
|
+ gatewayStatus = info.getString("paymentStatus");
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("AUTH_READY".equals(gatewayStatus) || "WAITING_AUTH".equals(gatewayStatus)) {
|
|
|
|
|
+ if (expired) {
|
|
|
|
|
+ int changed = paymentService.markManualReview(
|
|
|
|
|
+ payment.getId(), payment.getVersion(), payment.getStatus());
|
|
|
|
|
+ return changed == 1 ? "MANUAL_REVIEW" : reload(payment).getStatus();
|
|
|
|
|
+ }
|
|
|
|
|
+ int changed = paymentService.markOfflineWaitingAuth(payment.getId(), payment.getVersion(),
|
|
|
|
|
+ response.transactionId(), shortly(), waitingAuthDeadline(payment));
|
|
|
|
|
+ return changed == 1 ? "WAITING_AUTH" : reload(payment).getStatus();
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("COMPLETE".equals(gatewayStatus)) {
|
|
|
|
|
+ return applyCompletedFact(payment, response).getStatus();
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("CANCEL".equals(gatewayStatus)) {
|
|
|
|
|
+ int changed = paymentService.markTerminal(payment.getId(), payment.getVersion(),
|
|
|
|
|
+ payment.getStatus(), "CANCELLED_OR_EXPIRED");
|
|
|
|
|
+ return changed == 1 ? "CANCELLED_OR_EXPIRED" : reload(payment).getStatus();
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("FAIL".equals(gatewayStatus)) {
|
|
|
|
|
+ int changed = paymentService.markTerminal(payment.getId(), payment.getVersion(),
|
|
|
|
|
+ payment.getStatus(), "FAILED");
|
|
|
|
|
+ return changed == 1 ? "FAILED" : reload(payment).getStatus();
|
|
|
|
|
+ }
|
|
|
|
|
+ return rescheduleOrReview(payment, expired);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private LinePayOfflineResult applyCompletedFact(PosOrderLinePayment payment,
|
|
|
|
|
+ LinePayResponse response) {
|
|
|
|
|
+ JSONObject info = response.info();
|
|
|
|
|
+ String transactionId = info == null ? null : info.getString("transactionId");
|
|
|
|
|
+ String orderId = info == null ? null : info.getString("orderId");
|
|
|
|
|
+ Integer actualAmount = paymentAmount(info);
|
|
|
|
|
+ String provider = paymentProvider(info);
|
|
|
|
|
+ if (transactionId == null || transactionId.isBlank()) {
|
|
|
|
|
+ int changed = paymentService.markManualReview(
|
|
|
|
|
+ payment.getId(), payment.getVersion(), payment.getStatus());
|
|
|
|
|
+ if (changed != 1) {
|
|
|
|
|
+ return result(reload(payment), false);
|
|
|
|
|
+ }
|
|
|
|
|
+ payment.setStatus("MANUAL_REVIEW");
|
|
|
|
|
+ return result(payment, false);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!payment.getLineOrderId().equals(orderId) || actualAmount == null
|
|
|
|
|
+ || provider == null || provider.isBlank()) {
|
|
|
|
|
+ if (actualAmount == null) {
|
|
|
|
|
+ int changed = paymentService.markManualReview(
|
|
|
|
|
+ payment.getId(), payment.getVersion(), payment.getStatus());
|
|
|
|
|
+ if (changed != 1) {
|
|
|
|
|
+ return result(reload(payment), false);
|
|
|
|
|
+ }
|
|
|
|
|
+ payment.setStatus("MANUAL_REVIEW");
|
|
|
|
|
+ return result(payment, false);
|
|
|
|
|
+ }
|
|
|
|
|
+ int changed = paymentService.markOfflineEvidence(payment.getId(), payment.getVersion(),
|
|
|
|
|
+ payment.getStatus(), "MANUAL_REVIEW", transactionId, provider,
|
|
|
|
|
+ actualAmount, new Date());
|
|
|
|
|
+ if (changed != 1) {
|
|
|
|
|
+ return result(reload(payment), false);
|
|
|
|
|
+ }
|
|
|
|
|
+ payment.setTransactionId(transactionId);
|
|
|
|
|
+ payment.setPaymentProvider(provider);
|
|
|
|
|
+ payment.setCapturedAmount(actualAmount);
|
|
|
|
|
+ payment.setStatus("MANUAL_REVIEW");
|
|
|
|
|
+ return result(payment, false);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!payment.getAmount().equals(actualAmount)) {
|
|
|
|
|
+ boolean changed = factService.applyAmountMismatchFact(payment.getId(),
|
|
|
|
|
+ payment.getVersion(), payment.getStatus(), transactionId, provider,
|
|
|
|
|
+ actualAmount, new Date());
|
|
|
|
|
+ if (!changed) {
|
|
|
|
|
+ return result(reload(payment), false);
|
|
|
|
|
+ }
|
|
|
|
|
+ payment.setTransactionId(transactionId);
|
|
|
|
|
+ payment.setPaymentProvider(provider);
|
|
|
|
|
+ payment.setCapturedAmount(actualAmount);
|
|
|
|
|
+ payment.setStatus("AMOUNT_MISMATCH");
|
|
|
|
|
+ return result(payment, false);
|
|
|
|
|
+ }
|
|
|
|
|
+ factService.applyPaidFact(payment.getId(), transactionId, provider, new Date());
|
|
|
|
|
+ payment.setTransactionId(transactionId);
|
|
|
|
|
+ payment.setPaymentProvider(provider);
|
|
|
|
|
+ payment.setCapturedAmount(payment.getAmount());
|
|
|
|
|
+ payment.setStatus("PAID");
|
|
|
|
|
+ payment.setUpdateTime(new Date());
|
|
|
|
|
+ return result(payment, false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String rescheduleOrReview(PosOrderLinePayment payment, boolean expired) {
|
|
|
|
|
+ if (expired) {
|
|
|
|
|
+ int changed = paymentService.markManualReview(
|
|
|
|
|
+ payment.getId(), payment.getVersion(), payment.getStatus());
|
|
|
|
|
+ return changed == 1 ? "MANUAL_REVIEW" : reload(payment).getStatus();
|
|
|
|
|
+ }
|
|
|
|
|
+ int changed = paymentService.rescheduleOffline(
|
|
|
|
|
+ payment.getId(), payment.getVersion(), payment.getStatus(),
|
|
|
|
|
+ shortly(), payment.getReconcileDeadline() == null
|
|
|
|
|
+ ? unknownDeadline() : payment.getReconcileDeadline());
|
|
|
|
|
+ return changed == 1 ? payment.getStatus() : reload(payment).getStatus();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private IntentSelection createIntent(PosOrder order, PosStoreLinePay credential) {
|
|
|
|
|
+ for (int attempt = 0; attempt < 3; attempt++) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ return new IntentSelection(paymentService.createRequesting(String.valueOf(order.getDdId()),
|
|
|
|
|
+ newLineOrderId(), credential.getId(), order.getMdId(), order.getAmount(),
|
|
|
|
|
+ "TWD", MODE_OFFLINE, unknownDeadline()), true);
|
|
|
|
|
+ } catch (DuplicateKeyException duplicate) {
|
|
|
|
|
+ PosOrderLinePayment active = paymentService.getActiveByDdId(
|
|
|
|
|
+ String.valueOf(order.getDdId()));
|
|
|
|
|
+ if (active != null) {
|
|
|
|
|
+ if (!MODE_OFFLINE.equals(active.getPaymentMode())) {
|
|
|
|
|
+ throw new ServiceException(
|
|
|
|
|
+ MessageUtils.message("line.pay.offline.attempt.blocked"));
|
|
|
|
|
+ }
|
|
|
|
|
+ return new IntentSelection(active, false);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (attempt == 2) {
|
|
|
|
|
+ throw duplicate;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ throw new ServiceException(MessageUtils.message("line.pay.offline.operation.failed"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private PosOrder requireMerchantOrder(Long merchantId, String ddId, boolean requirePayable) {
|
|
|
|
|
+ if (merchantId == null) {
|
|
|
|
|
+ throw new ServiceException(MessageUtils.message("line.pay.offline.merchant.required"));
|
|
|
|
|
+ }
|
|
|
|
|
+ InfoUser merchant = userService.getOne(new QueryWrapper<InfoUser>()
|
|
|
|
|
+ .eq("user_id", merchantId));
|
|
|
|
|
+ if (merchant == null || "0".equals(merchant.getUserType())
|
|
|
|
|
+ || "2".equals(merchant.getUserType())) {
|
|
|
|
|
+ throw new ServiceException(MessageUtils.message("line.pay.offline.merchant.required"));
|
|
|
|
|
+ }
|
|
|
|
|
+ PosOrder order = orderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", ddId));
|
|
|
|
|
+ if (order == null || !"MERCHANT".equals(order.getOrderSource())) {
|
|
|
|
|
+ throw new ServiceException(MessageUtils.message("line.pay.offline.order.forbidden"));
|
|
|
|
|
+ }
|
|
|
|
|
+ boolean directMerchant = "1".equals(merchant.getUserType())
|
|
|
|
|
+ || "3".equals(merchant.getUserType());
|
|
|
|
|
+ PosStore store = storeService.getById(order.getMdId());
|
|
|
|
|
+ if (store == null) {
|
|
|
|
|
+ throw new ServiceException(MessageUtils.message("line.pay.offline.order.forbidden"));
|
|
|
|
|
+ }
|
|
|
|
|
+ boolean owned = directMerchant
|
|
|
|
|
+ ? merchant.getUserId().equals(order.getShId())
|
|
|
|
|
+ && merchant.getUserId().equals(store.getUserId())
|
|
|
|
|
+ : merchant.getStoreId() != null && merchant.getStoreId().equals(order.getMdId());
|
|
|
|
|
+ if (!owned) {
|
|
|
|
|
+ throw new ServiceException(MessageUtils.message("line.pay.offline.order.forbidden"));
|
|
|
|
|
+ }
|
|
|
|
|
+ String parentDdId = order.getParentDdId() == null
|
|
|
|
|
+ ? String.valueOf(order.getDdId()) : order.getParentDdId();
|
|
|
|
|
+ if (orderService.count(new QueryWrapper<PosOrder>()
|
|
|
|
|
+ .eq("parent_dd_id", parentDdId)) > 1) {
|
|
|
|
|
+ throw new ServiceException(MessageUtils.message("line.pay.offline.single.store.required"));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (requirePayable && (!LinePayService.PAY_TYPE_LINE.equals(order.getPayType())
|
|
|
|
|
+ || order.getPayStatus() != null && order.getPayStatus() != 0L
|
|
|
|
|
+ || order.getState() == null || order.getState() == 3L || order.getState() == 4L
|
|
|
|
|
+ || order.getAmount() == null || order.getAmount() <= 0 || order.getMdId() == null
|
|
|
|
|
+ || order.getAfterSaleStatus() != null && order.getAfterSaleStatus() != 0L)) {
|
|
|
|
|
+ throw new ServiceException(MessageUtils.message("line.pay.offline.order.invalid"));
|
|
|
|
|
+ }
|
|
|
|
|
+ return order;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Integer paymentAmount(JSONObject info) {
|
|
|
|
|
+ if (info == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONArray payInfo = info.getJSONArray("payInfo");
|
|
|
|
|
+ if (payInfo == null || payInfo.isEmpty()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ long total = 0L;
|
|
|
|
|
+ for (int i = 0; i < payInfo.size(); i++) {
|
|
|
|
|
+ JSONObject item = payInfo.getJSONObject(i);
|
|
|
|
|
+ Integer amount = item == null ? null : item.getInteger("amount");
|
|
|
|
|
+ if (amount == null || amount <= 0) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ total += amount;
|
|
|
|
|
+ }
|
|
|
|
|
+ return total <= Integer.MAX_VALUE ? (int) total : null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String paymentProvider(JSONObject info) {
|
|
|
|
|
+ if (info == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ String provider = info.getString("paymentProvider");
|
|
|
|
|
+ if (provider != null && !provider.isBlank()) {
|
|
|
|
|
+ return provider;
|
|
|
|
|
+ }
|
|
|
|
|
+ JSONArray payInfo = info.getJSONArray("payInfo");
|
|
|
|
|
+ if (payInfo == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (int i = 0; i < payInfo.size(); i++) {
|
|
|
|
|
+ JSONObject item = payInfo.getJSONObject(i);
|
|
|
|
|
+ String itemProvider = item == null ? null : item.getString("paymentProvider");
|
|
|
|
|
+ if (itemProvider != null && !itemProvider.isBlank()) {
|
|
|
|
|
+ return itemProvider;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static boolean isDefinitePayFailure(LinePayResponse response) {
|
|
|
|
|
+ if (response == null || response.httpStatus() < 200 || response.httpStatus() >= 300
|
|
|
|
|
+ || response.returnCode() == null || response.isSuccess()
|
|
|
|
|
+ || AUTH_REQUIRED_CODES.contains(response.returnCode())
|
|
|
|
|
+ || UNCERTAIN_CODES.contains(response.returnCode())
|
|
|
|
|
+ || response.returnCode().startsWith("190")) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return DEFINITE_PAY_FAILURE_CODES.contains(response.returnCode());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static LinePayCredential toCredential(PosStoreLinePay credential) {
|
|
|
|
|
+ return new LinePayCredential(credential.getId(), credential.getChannelId(),
|
|
|
|
|
+ credential.getChannelSecret());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String normalizeDdId(String ddId) {
|
|
|
|
|
+ if (ddId == null || ddId.trim().isEmpty()) {
|
|
|
|
|
+ throw new ServiceException(MessageUtils.message("line.pay.order.required"));
|
|
|
|
|
+ }
|
|
|
|
|
+ return ddId.trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String newLineOrderId() {
|
|
|
|
|
+ return "LPOFF" + UUID.randomUUID().toString().replace("-", "")
|
|
|
|
|
+ .toUpperCase(Locale.ROOT);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Date shortly() {
|
|
|
|
|
+ return new Date(System.currentTimeMillis() + 30_000L);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Date authDeadline() {
|
|
|
|
|
+ return new Date(System.currentTimeMillis() + authDeadlineMinutes * 60_000L);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Date unknownDeadline() {
|
|
|
|
|
+ return new Date(System.currentTimeMillis() + unknownDeadlineHours * 3_600_000L);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Date waitingAuthDeadline(PosOrderLinePayment payment) {
|
|
|
|
|
+ return "WAITING_AUTH".equals(payment.getStatus())
|
|
|
|
|
+ && payment.getReconcileDeadline() != null
|
|
|
|
|
+ ? payment.getReconcileDeadline() : authDeadline();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private PosOrderLinePayment reload(PosOrderLinePayment fallback) {
|
|
|
|
|
+ PosOrderLinePayment current = paymentService.getById(fallback.getId());
|
|
|
|
|
+ return current == null ? fallback : current;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static LinePayOfflineResult result(PosOrderLinePayment payment, Boolean reused) {
|
|
|
|
|
+ LinePayOfflineResult result = new LinePayOfflineResult();
|
|
|
|
|
+ result.setDdId(payment.getDdId());
|
|
|
|
|
+ result.setPaymentId(payment.getId());
|
|
|
|
|
+ result.setLineOrderId(payment.getLineOrderId());
|
|
|
|
|
+ result.setTransactionId(payment.getTransactionId());
|
|
|
|
|
+ result.setStatus(publicStatus(payment.getStatus()));
|
|
|
|
|
+ result.setReusedAttempt(reused);
|
|
|
|
|
+ result.setUpdatedAt(payment.getUpdateTime());
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String publicStatus(String status) {
|
|
|
|
|
+ if ("WAITING_AUTH".equals(status)) {
|
|
|
|
|
+ return "AUTH_REQUIRED";
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("REQUESTING".equals(status) || "REQUEST_UNKNOWN".equals(status)) {
|
|
|
|
|
+ return "PROCESSING";
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("CANCELLED_OR_EXPIRED".equals(status)) {
|
|
|
|
|
+ return "CANCELLED";
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("AMOUNT_MISMATCH".equals(status) || "MANUAL_REVIEW".equals(status)) {
|
|
|
|
|
+ return "MANUAL_REVIEW";
|
|
|
|
|
+ }
|
|
|
|
|
+ return status;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private record IntentSelection(PosOrderLinePayment payment, boolean created) {
|
|
|
|
|
+ }
|
|
|
|
|
+}
|