LinePayService.java 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  1. package com.ruoyi.app.pay;
  2. import com.alibaba.fastjson2.JSONObject;
  3. import com.alibaba.fastjson2.JSONArray;
  4. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  5. import com.ruoyi.app.pay.dto.LinePayCreateResult;
  6. import com.ruoyi.app.pay.dto.LinePayQueryResult;
  7. import com.ruoyi.app.order.MultiStoreCheckoutOnlineFactService;
  8. import com.ruoyi.app.utils.linepay.LinePayClient;
  9. import com.ruoyi.app.utils.linepay.LinePayCredential;
  10. import com.ruoyi.app.utils.linepay.LinePayRequest;
  11. import com.ruoyi.app.utils.linepay.LinePayResponse;
  12. import com.ruoyi.common.exception.ServiceException;
  13. import com.ruoyi.system.domain.PosOrder;
  14. import com.ruoyi.system.domain.PosOrderLinePayment;
  15. import com.ruoyi.system.domain.PosOrderLineRefund;
  16. import com.ruoyi.system.domain.PosStoreLinePay;
  17. import com.ruoyi.system.service.IPosOrderLinePaymentService;
  18. import com.ruoyi.system.service.IPosOrderLineRefundService;
  19. import com.ruoyi.system.service.IPosOrderService;
  20. import com.ruoyi.system.service.IPosStoreLinePayService;
  21. import org.springframework.dao.DuplicateKeyException;
  22. import org.springframework.beans.factory.annotation.Autowired;
  23. import org.springframework.stereotype.Service;
  24. import org.springframework.beans.factory.annotation.Value;
  25. import java.util.Comparator;
  26. import java.util.Date;
  27. import java.util.List;
  28. import java.util.Locale;
  29. import java.util.Objects;
  30. import java.util.Set;
  31. import java.util.UUID;
  32. /** LINE Pay create, query, confirm, retrieval and payment-fact orchestration. */
  33. @Service
  34. public class LinePayService {
  35. public static final String PAY_TYPE_LINE = "3";
  36. private static final List<String> DEFINITE_REQUEST_FAILURE_CODES = List.of(
  37. "1104", "1105", "1106", "1124", "1178", "1183", "1194", "2101", "2102");
  38. private static final Set<String> CREDENTIAL_AUTH_FAILURE_CODES = Set.of("1104", "1105", "1106");
  39. @Value("${line-pay.reconcile.auth-deadline-minutes:30}")
  40. private long authDeadlineMinutes = 30L;
  41. @Value("${line-pay.reconcile.unknown-deadline-hours:24}")
  42. private long unknownDeadlineHours = 24L;
  43. @Value("${line-pay.reconcile.row-lease-seconds:120}")
  44. private long rowLeaseSeconds = 120L;
  45. private final IPosOrderService orderService;
  46. private final IPosStoreLinePayService credentialService;
  47. private final IPosOrderLinePaymentService paymentService;
  48. private final IPosOrderLineRefundService refundService;
  49. private final LinePayClient linePayClient;
  50. private final LinePayFactService factService;
  51. private final LinePayGatewayAuditService auditService;
  52. private final PaymentCreateGuardService createGuard;
  53. private final LinePayOfflineService offlineService;
  54. private final MultiStoreCheckoutOnlineFactService multiStoreFacts;
  55. public LinePayService(IPosOrderService orderService,
  56. IPosStoreLinePayService credentialService,
  57. IPosOrderLinePaymentService paymentService,
  58. IPosOrderLineRefundService refundService,
  59. LinePayClient linePayClient,
  60. LinePayFactService factService) {
  61. this(orderService, credentialService, paymentService, refundService,
  62. linePayClient, factService, null, null, null, null);
  63. }
  64. public LinePayService(IPosOrderService orderService,
  65. IPosStoreLinePayService credentialService,
  66. IPosOrderLinePaymentService paymentService,
  67. IPosOrderLineRefundService refundService,
  68. LinePayClient linePayClient,
  69. LinePayFactService factService,
  70. LinePayGatewayAuditService auditService,
  71. PaymentCreateGuardService createGuard,
  72. LinePayOfflineService offlineService) {
  73. this(orderService, credentialService, paymentService, refundService, linePayClient, factService,
  74. auditService, createGuard, offlineService, null);
  75. }
  76. @Autowired
  77. public LinePayService(IPosOrderService orderService,
  78. IPosStoreLinePayService credentialService,
  79. IPosOrderLinePaymentService paymentService,
  80. IPosOrderLineRefundService refundService,
  81. LinePayClient linePayClient,
  82. LinePayFactService factService,
  83. LinePayGatewayAuditService auditService,
  84. PaymentCreateGuardService createGuard,
  85. LinePayOfflineService offlineService,
  86. MultiStoreCheckoutOnlineFactService multiStoreFacts) {
  87. this.orderService = orderService;
  88. this.credentialService = credentialService;
  89. this.paymentService = paymentService;
  90. this.refundService = refundService;
  91. this.linePayClient = linePayClient;
  92. this.factService = factService;
  93. this.auditService = auditService;
  94. this.createGuard = createGuard;
  95. this.offlineService = offlineService;
  96. this.multiStoreFacts = multiStoreFacts;
  97. }
  98. public LinePayCreateResult create(Long userId, String ddId) {
  99. if (createGuard != null) {
  100. return createGuard.withLock(ddId, () -> createUnderLock(userId, ddId));
  101. }
  102. return createUnderLock(userId, ddId);
  103. }
  104. private LinePayCreateResult createUnderLock(Long userId, String ddId) {
  105. PosOrder order = requirePayableOrder(userId, ddId);
  106. requireSingleStoreOrder(order, userId);
  107. PosStoreLinePay credential = credentialService.getEnabledCurrent(order.getMdId());
  108. if (credential == null) {
  109. throw new ServiceException("LINE Pay is not enabled for this store");
  110. }
  111. PosOrderLinePayment active = paymentService.getActiveByDdId(ddId);
  112. if (active != null) {
  113. return createResult(active, true);
  114. }
  115. IntentSelection selection = createIntent(order, credential);
  116. PosOrderLinePayment intent = selection.payment();
  117. if (!selection.created()) {
  118. return createResult(intent, true);
  119. }
  120. if (multiStoreFacts != null && isMultiStoreCandidate(order)) {
  121. // 在实际调用 LINE Request API 前持久化发起证据,缓冲期只承认此时点已发出的交易。
  122. multiStoreFacts.recordDispatch(order.getId(), "LINE", PAY_TYPE_LINE, intent.getLineOrderId());
  123. }
  124. try {
  125. LinePayResponse response = gatewayCall("REQUEST", "APP", intent,
  126. () -> linePayClient.request(toCredential(credential),
  127. new LinePayRequest(intent.getLineOrderId(), ddId,
  128. "Order " + ddId, order.getAmount(), "TWD")));
  129. if (!response.isSuccess() || response.transactionId() == null) {
  130. if (isDefiniteRequestFailure(response)) {
  131. if (paymentService.markTerminal(intent.getId(), intent.getVersion(),
  132. "REQUESTING", "FAILED") == 1) {
  133. intent.setStatus("FAILED");
  134. intent.setActiveDdId(null);
  135. return createResult(intent, false);
  136. }
  137. PosOrderLinePayment latest = paymentService.getById(intent.getId());
  138. if (latest != null) {
  139. return createResult(latest, true);
  140. }
  141. throw new ServiceException("LINE Pay payment state requires reconciliation");
  142. }
  143. paymentService.markRequestUnknown(intent.getId(), intent.getVersion(),
  144. shortly(), unknownDeadline());
  145. intent.setStatus("REQUEST_UNKNOWN");
  146. return createResult(intent, false);
  147. }
  148. JSONObject paymentUrl = response.info() == null
  149. ? null : response.info().getJSONObject("paymentUrl");
  150. String web = paymentUrl == null ? null : paymentUrl.getString("web");
  151. String app = paymentUrl == null ? null : paymentUrl.getString("app");
  152. if (web == null || paymentService.markRequestSucceeded(intent.getId(), intent.getVersion(),
  153. response.transactionId(), web, app, shortly(), authDeadline()) != 1) {
  154. paymentService.markRequestUnknown(intent.getId(), intent.getVersion(),
  155. shortly(), unknownDeadline());
  156. intent.setStatus("REQUEST_UNKNOWN");
  157. return createResult(intent, false);
  158. }
  159. intent.setTransactionId(response.transactionId());
  160. intent.setPaymentUrlWeb(web);
  161. intent.setPaymentUrlApp(app);
  162. intent.setStatus("WAITING_AUTH");
  163. return createResult(intent, false);
  164. } catch (Exception unknown) {
  165. paymentService.markRequestUnknown(intent.getId(), intent.getVersion(),
  166. shortly(), unknownDeadline());
  167. intent.setStatus("REQUEST_UNKNOWN");
  168. return createResult(intent, false);
  169. }
  170. }
  171. public LinePayQueryResult query(Long userId, String ddId) {
  172. PosOrder order = requireOwnedOrder(userId, ddId);
  173. List<PosOrderLinePayment> attempts = paymentService.getByDdId(ddId);
  174. PosOrderLinePayment selected = selectForApp(order, attempts);
  175. LinePayQueryResult result = new LinePayQueryResult();
  176. result.setDdId(ddId);
  177. result.setOrderPayStatus(order.getPayStatus());
  178. if (selected != null) {
  179. result.setPaymentId(selected.getId());
  180. result.setPaymentStatus(selected.getStatus());
  181. result.setTransactionId(selected.getTransactionId());
  182. result.setUpdatedAt(selected.getUpdateTime());
  183. PosOrderLineRefund refund = refundService.getByPaymentId(selected.getId());
  184. result.setRefundStatus(refund == null ? null : refund.getStatus());
  185. }
  186. return result;
  187. }
  188. public PosOrderLinePayment findByLineOrderId(String lineOrderId, String transactionId) {
  189. PosOrderLinePayment payment = paymentService.getByLineOrderId(lineOrderId);
  190. if (payment == null) {
  191. return null;
  192. }
  193. if (transactionId != null && payment.getTransactionId() != null
  194. && !transactionId.equals(payment.getTransactionId())) {
  195. return null;
  196. }
  197. return payment;
  198. }
  199. public String reconcilePayment(Long paymentId, String source) {
  200. return reconcilePayment(paymentId, source, null);
  201. }
  202. /** 支付方式切换前只读取网关收款事实;不确认付款,查询失败则拒绝切换。 */
  203. public boolean isCapturedForSwitch(PosOrderLinePayment payment) {
  204. PosStoreLinePay credential = credentialService.getById(payment.getCredentialId());
  205. if (credential == null) {
  206. throw new IllegalStateException("LINE Pay credential version not found");
  207. }
  208. try {
  209. LinePayResponse response = retrieveWithCredentialRecovery(payment, "PAY_SWITCH",
  210. credential, toCredential(credential)).response();
  211. if (response == null || !(response.isSuccess() || "1150".equals(response.returnCode()))) {
  212. throw new IllegalStateException("LINE Pay switch query is inconclusive");
  213. }
  214. return capturedPayment(response, payment) != null;
  215. } catch (Exception error) {
  216. throw new IllegalStateException("LINE Pay switch query failed", error);
  217. }
  218. }
  219. public String reconcilePayment(Long paymentId, String source, String reconcileLeaseOwner) {
  220. PosOrderLinePayment payment = paymentService.getById(paymentId);
  221. if (payment == null) {
  222. throw new ServiceException("LINE Pay payment not found");
  223. }
  224. if ("OFFLINE".equals(payment.getPaymentMode()) && offlineService != null) {
  225. return offlineService.reconcilePayment(paymentId, source);
  226. }
  227. if (payment.getReconcileDeadline() != null
  228. && !new Date().before(payment.getReconcileDeadline())) {
  229. return finalDeadlineReconcile(payment, source);
  230. }
  231. PosStoreLinePay credential = credentialService.getById(payment.getCredentialId());
  232. if (credential == null) {
  233. throw new ServiceException("LINE Pay credential version not found");
  234. }
  235. try {
  236. RetrieveResult retrieveResult = retrieveWithCredentialRecovery(
  237. payment, source, credential, toCredential(credential));
  238. LinePayResponse retrieve = retrieveResult.response();
  239. LinePayCredential gatewayCredential = retrieveResult.credential();
  240. JSONObject captured = capturedPayment(retrieve, payment);
  241. if (captured != null) {
  242. String fullRefundTransactionId = fullRefundTransactionId(captured, payment.getAmount());
  243. if (fullRefundTransactionId != null) {
  244. factService.applyPaidAndRefundedFact(payment.getId(),
  245. captured.getString("transactionId"), paymentProvider(retrieve), new Date(),
  246. fullRefundTransactionId, new Date());
  247. return "REFUNDED";
  248. }
  249. if (hasRefundEvidence(captured)) {
  250. if (isLocallyRefunded(payment.getId())) {
  251. return "REFUNDED";
  252. }
  253. factService.applyPaidWithRefundReviewFact(payment.getId(),
  254. captured.getString("transactionId"), paymentProvider(retrieve), new Date());
  255. return "MANUAL_REVIEW";
  256. }
  257. factService.applyPaidFact(payment.getId(), captured.getString("transactionId"),
  258. paymentProvider(retrieve), new Date());
  259. return "PAID";
  260. }
  261. if (!("1150".equals(retrieve.returnCode()) || retrieve.isSuccess())) {
  262. return payment.getStatus();
  263. }
  264. if (!"1150".equals(retrieve.returnCode())
  265. || payment.getTransactionId() == null
  266. || !canCheckOrConfirm(payment.getStatus())) {
  267. return payment.getStatus();
  268. }
  269. LinePayResponse check = gatewayCall("CHECK", source, payment,
  270. () -> linePayClient.check(gatewayCredential, payment.getTransactionId()));
  271. return handleCheck(payment, gatewayCredential, check, source, reconcileLeaseOwner);
  272. } catch (Exception unknown) {
  273. if ("CONFIRMING".equals(payment.getStatus())) {
  274. paymentService.markConfirmUnknown(payment.getId(), payment.getVersion(),
  275. shortly(), unknownDeadline());
  276. return "CONFIRM_UNKNOWN";
  277. }
  278. return payment.getStatus();
  279. }
  280. }
  281. private String finalDeadlineReconcile(PosOrderLinePayment payment, String source) {
  282. PosStoreLinePay credential = credentialService.getById(payment.getCredentialId());
  283. if (credential == null) {
  284. paymentService.markManualReview(payment.getId(), payment.getVersion(), payment.getStatus());
  285. return "MANUAL_REVIEW";
  286. }
  287. try {
  288. RetrieveResult retrieveResult = retrieveWithCredentialRecovery(
  289. payment, source, credential, toCredential(credential));
  290. LinePayCredential gatewayCredential = retrieveResult.credential();
  291. LinePayResponse retrieve = retrieveResult.response();
  292. JSONObject captured = capturedPayment(retrieve, payment);
  293. if (captured != null) {
  294. String fullRefundTransactionId = fullRefundTransactionId(captured, payment.getAmount());
  295. if (fullRefundTransactionId != null) {
  296. factService.applyPaidAndRefundedFact(payment.getId(),
  297. captured.getString("transactionId"), paymentProvider(retrieve), new Date(),
  298. fullRefundTransactionId, new Date());
  299. return "REFUNDED";
  300. }
  301. if (hasRefundEvidence(captured)) {
  302. if (isLocallyRefunded(payment.getId())) {
  303. return "REFUNDED";
  304. }
  305. factService.applyPaidWithRefundReviewFact(payment.getId(),
  306. captured.getString("transactionId"), paymentProvider(retrieve), new Date());
  307. return "MANUAL_REVIEW";
  308. }
  309. factService.applyPaidFact(payment.getId(), captured.getString("transactionId"),
  310. paymentProvider(retrieve), new Date());
  311. return "PAID";
  312. }
  313. if ("1150".equals(retrieve.returnCode()) && payment.getTransactionId() != null
  314. && canCheckOrConfirm(payment.getStatus())) {
  315. LinePayResponse check = gatewayCall("CHECK", source, payment,
  316. () -> linePayClient.check(gatewayCredential, payment.getTransactionId()));
  317. if ("0121".equals(check.returnCode()) || "0122".equals(check.returnCode())) {
  318. String terminal = "0121".equals(check.returnCode())
  319. ? "CANCELLED_OR_EXPIRED" : "FAILED";
  320. paymentService.markTerminal(payment.getId(), payment.getVersion(),
  321. payment.getStatus(), terminal);
  322. return terminal;
  323. }
  324. }
  325. } catch (Exception ignored) {
  326. // The final read was inconclusive; stop automatic scanning for manual review.
  327. }
  328. paymentService.markManualReview(payment.getId(), payment.getVersion(), payment.getStatus());
  329. return "MANUAL_REVIEW";
  330. }
  331. private String handleCheck(PosOrderLinePayment payment, LinePayCredential credential,
  332. LinePayResponse check, String source,
  333. String reconcileLeaseOwner) throws Exception {
  334. String code = check.returnCode();
  335. if ("0000".equals(code)) {
  336. return payment.getStatus();
  337. }
  338. if ("0121".equals(code) || "0122".equals(code)) {
  339. String terminal = "0121".equals(code) ? "CANCELLED_OR_EXPIRED" : "FAILED";
  340. paymentService.markTerminal(payment.getId(), payment.getVersion(), payment.getStatus(), terminal);
  341. return terminal;
  342. }
  343. if ("0123".equals(code)) {
  344. LinePayResponse retrieve = gatewayCall("RETRIEVE", source, payment,
  345. () -> payment.getTransactionId() == null
  346. ? linePayClient.retrieveByOrderId(credential, payment.getLineOrderId())
  347. : linePayClient.retrieveByTransactionId(credential, payment.getTransactionId()));
  348. JSONObject captured = capturedPayment(retrieve, payment);
  349. if (captured != null) {
  350. String fullRefundTransactionId = fullRefundTransactionId(captured, payment.getAmount());
  351. if (fullRefundTransactionId != null) {
  352. factService.applyPaidAndRefundedFact(payment.getId(),
  353. captured.getString("transactionId"), paymentProvider(retrieve), new Date(),
  354. fullRefundTransactionId, new Date());
  355. return "REFUNDED";
  356. }
  357. if (hasRefundEvidence(captured)) {
  358. if (isLocallyRefunded(payment.getId())) {
  359. return "REFUNDED";
  360. }
  361. factService.applyPaidWithRefundReviewFact(payment.getId(),
  362. captured.getString("transactionId"), paymentProvider(retrieve), new Date());
  363. return "MANUAL_REVIEW";
  364. }
  365. factService.applyPaidFact(payment.getId(), captured.getString("transactionId"),
  366. paymentProvider(retrieve), new Date());
  367. return "PAID";
  368. }
  369. return payment.getStatus();
  370. }
  371. if (!"0110".equals(code)) {
  372. return payment.getStatus();
  373. }
  374. if (!canCheckOrConfirm(payment.getStatus())) {
  375. return payment.getStatus();
  376. }
  377. PosOrder order = orderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", payment.getDdId()));
  378. if (order == null || !isPayableOrderState(order)) {
  379. paymentService.markAuthDoneOrderCancelled(payment.getId(), payment.getVersion(),
  380. payment.getStatus(), shortly(), authDeadline());
  381. return "AUTH_DONE_ORDER_CANCELLED";
  382. }
  383. if (!LinePayService.PAY_TYPE_LINE.equals(order.getPayType())
  384. || Long.valueOf(1L).equals(order.getPayStatus())
  385. || !order.getMdId().equals(payment.getStoreId())
  386. || !order.getAmount().equals(payment.getAmount())) {
  387. return payment.getStatus();
  388. }
  389. String leaseOwner = reconcileLeaseOwner == null
  390. ? (source == null ? "LINE" : source) + "-" + UUID.randomUUID()
  391. : reconcileLeaseOwner;
  392. if (paymentService.claimConfirm(payment.getId(), payment.getVersion(), payment.getStatus(),
  393. leaseOwner, leaseUntil()) != 1) {
  394. return payment.getStatus();
  395. }
  396. LinePayResponse confirm;
  397. try {
  398. confirm = gatewayCall("CONFIRM", source, payment,
  399. () -> linePayClient.confirm(credential, payment.getTransactionId(),
  400. payment.getAmount(), payment.getCurrency()));
  401. } catch (Exception unknown) {
  402. paymentService.markConfirmUnknown(payment.getId(), payment.getVersion() + 1,
  403. shortly(), unknownDeadline());
  404. return "CONFIRM_UNKNOWN";
  405. }
  406. if (confirm.isSuccess()) {
  407. if (!confirmedPayment(confirm, payment)) {
  408. paymentService.markConfirmUnknown(payment.getId(), payment.getVersion() + 1,
  409. shortly(), unknownDeadline());
  410. return "CONFIRM_UNKNOWN";
  411. }
  412. factService.applyPaidFact(payment.getId(), payment.getTransactionId(),
  413. paymentProvider(confirm), new Date());
  414. return "PAID";
  415. }
  416. paymentService.markConfirmUnknown(payment.getId(), payment.getVersion() + 1,
  417. shortly(), unknownDeadline());
  418. return "CONFIRM_UNKNOWN";
  419. }
  420. private static JSONObject capturedPayment(LinePayResponse response, PosOrderLinePayment payment) {
  421. if (response == null || !response.isSuccess()) {
  422. return null;
  423. }
  424. JSONObject root = JSONObject.parseObject(response.rawBody());
  425. JSONArray info = root.getJSONArray("info");
  426. if (info == null || info.size() != 1) {
  427. return null;
  428. }
  429. JSONObject transaction = info.getJSONObject(0);
  430. String gatewayTransactionId = transaction.getString("transactionId");
  431. boolean transactionMatches = payment.getTransactionId() == null
  432. ? gatewayTransactionId != null
  433. : payment.getTransactionId().equals(gatewayTransactionId);
  434. Integer gatewayAmount = paymentAmount(transaction);
  435. return transactionMatches
  436. && payment.getLineOrderId().equals(transaction.getString("orderId"))
  437. && payment.getCurrency().equals(transaction.getString("currency"))
  438. && "PAYMENT".equals(transaction.getString("transactionType"))
  439. && gatewayAmount != null && gatewayAmount.equals(payment.getAmount())
  440. ? transaction : null;
  441. }
  442. private static Integer paymentAmount(JSONObject transaction) {
  443. JSONArray payInfo = transaction.getJSONArray("payInfo");
  444. if (payInfo == null || payInfo.isEmpty()) {
  445. return null;
  446. }
  447. long total = 0L;
  448. for (int i = 0; i < payInfo.size(); i++) {
  449. JSONObject item = payInfo.getJSONObject(i);
  450. Integer amount = item == null ? null : item.getInteger("amount");
  451. if (amount == null || amount <= 0) {
  452. return null;
  453. }
  454. total += amount;
  455. }
  456. return total <= Integer.MAX_VALUE ? (int) total : null;
  457. }
  458. private static String fullRefundTransactionId(JSONObject transaction, int expectedAmount) {
  459. JSONArray refunds = transaction.getJSONArray("refundList");
  460. if (refunds == null || refunds.isEmpty()) {
  461. return null;
  462. }
  463. long total = 0L;
  464. String lastTransactionId = null;
  465. for (int i = 0; i < refunds.size(); i++) {
  466. JSONObject refund = refunds.getJSONObject(i);
  467. Integer amount = refund == null ? null : refund.getInteger("refundAmount");
  468. String transactionId = refund == null ? null : refund.getString("refundTransactionId");
  469. if (amount == null || amount == 0 || transactionId == null) {
  470. return null;
  471. }
  472. total += Math.abs((long) amount);
  473. lastTransactionId = transactionId;
  474. }
  475. return total == expectedAmount ? lastTransactionId : null;
  476. }
  477. private static boolean hasRefundEvidence(JSONObject transaction) {
  478. JSONArray refunds = transaction.getJSONArray("refundList");
  479. return refunds != null && !refunds.isEmpty();
  480. }
  481. private static boolean canCheckOrConfirm(String status) {
  482. return "WAITING_AUTH".equals(status) || "READY_CONFIRM".equals(status);
  483. }
  484. private static String paymentProvider(LinePayResponse response) {
  485. if (response == null || response.rawBody() == null) {
  486. return null;
  487. }
  488. JSONObject root = JSONObject.parseObject(response.rawBody());
  489. JSONObject objectInfo = root.getJSONObject("info");
  490. if (objectInfo != null) {
  491. return objectInfo.getString("paymentProvider");
  492. }
  493. JSONArray info = root.getJSONArray("info");
  494. return info != null && info.size() == 1
  495. ? info.getJSONObject(0).getString("paymentProvider") : null;
  496. }
  497. private static boolean confirmedPayment(LinePayResponse response, PosOrderLinePayment payment) {
  498. if (response == null || !response.isSuccess() || response.rawBody() == null) {
  499. return false;
  500. }
  501. JSONObject info = JSONObject.parseObject(response.rawBody()).getJSONObject("info");
  502. if (info == null || !payment.getTransactionId().equals(info.getString("transactionId"))) {
  503. return false;
  504. }
  505. String orderId = info.getString("orderId");
  506. String currency = info.getString("currency");
  507. return (orderId == null || payment.getLineOrderId().equals(orderId))
  508. && (currency == null || payment.getCurrency().equals(currency))
  509. && payment.getAmount().equals(paymentAmount(info));
  510. }
  511. private IntentSelection createIntent(PosOrder order, PosStoreLinePay credential) {
  512. for (int attempt = 0; attempt < 3; attempt++) {
  513. try {
  514. return new IntentSelection(paymentService.createRequesting(
  515. String.valueOf(order.getDdId()), newLineOrderId(), credential.getId(),
  516. order.getMdId(), order.getAmount(), "TWD", "ONLINE", unknownDeadline()), true);
  517. } catch (DuplicateKeyException duplicate) {
  518. PosOrderLinePayment concurrent = paymentService.getActiveByDdId(String.valueOf(order.getDdId()));
  519. if (concurrent != null) {
  520. return new IntentSelection(concurrent, false);
  521. }
  522. if (attempt == 2) {
  523. throw duplicate;
  524. }
  525. }
  526. }
  527. throw new IllegalStateException("Unable to create LINE Pay attempt");
  528. }
  529. private record IntentSelection(PosOrderLinePayment payment, boolean created) {
  530. }
  531. private PosOrder requirePayableOrder(Long userId, String ddId) {
  532. PosOrder order = requireOwnedOrder(userId, ddId);
  533. if (!PAY_TYPE_LINE.equals(order.getPayType()) || Long.valueOf(1L).equals(order.getPayStatus())
  534. || !isPayableOrderState(order) || order.getAmount() == null
  535. || order.getAmount() <= 0 || order.getMdId() == null) {
  536. throw new ServiceException("Order is not payable by LINE Pay");
  537. }
  538. return order;
  539. }
  540. private static boolean isPayableOrderState(PosOrder order) {
  541. return order.getState() != null && (order.getState() == 0L || order.getState() == 1L)
  542. && (order.getAfterSaleStatus() == null || order.getAfterSaleStatus() == 0L);
  543. }
  544. private static boolean isDefiniteRequestFailure(LinePayResponse response) {
  545. if (response == null || response.httpStatus() < 200 || response.httpStatus() >= 300
  546. || response.returnCode() == null || response.isSuccess()) {
  547. return false;
  548. }
  549. return DEFINITE_REQUEST_FAILURE_CODES.contains(response.returnCode());
  550. }
  551. private Date leaseUntil() {
  552. return new Date(System.currentTimeMillis() + Math.max(60L, rowLeaseSeconds) * 1000L);
  553. }
  554. private PosOrder requireOwnedOrder(Long userId, String ddId) {
  555. if (userId == null || ddId == null || ddId.trim().isEmpty()) {
  556. throw new ServiceException("Invalid order");
  557. }
  558. PosOrder order = orderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", ddId));
  559. if (order == null || order.getUserId() == null || !order.getUserId().equals(userId)) {
  560. throw new ServiceException("Order not found or forbidden");
  561. }
  562. return order;
  563. }
  564. private void requireSingleStoreOrder(PosOrder order, Long userId) {
  565. String parentDdId = order.getParentDdId() == null
  566. ? String.valueOf(order.getDdId()) : order.getParentDdId();
  567. long count = orderService.count(new QueryWrapper<PosOrder>().eq("parent_dd_id", parentDdId));
  568. if (count > 1 && (multiStoreFacts == null || !isMultiStoreCandidate(order)
  569. || !multiStoreFacts.authorizeCreate(String.valueOf(order.getDdId()), userId, PAY_TYPE_LINE))) {
  570. throw new ServiceException("LINE Pay supports single-store orders only");
  571. }
  572. }
  573. private boolean isMultiStoreCandidate(PosOrder order) {
  574. return order != null && order.getDdId() != null && multiStoreFacts != null && multiStoreFacts.isBatchOrder(String.valueOf(order.getDdId()))
  575. && order.getParentDdId() != null && !order.getParentDdId().equals(order.getDdId());
  576. }
  577. private PosOrderLinePayment selectForApp(PosOrder order, List<PosOrderLinePayment> attempts) {
  578. if (attempts == null || attempts.isEmpty()) {
  579. return null;
  580. }
  581. List<PosOrderLinePayment> paid = attempts.stream()
  582. .filter(payment -> "PAID".equals(payment.getStatus())).toList();
  583. List<PosOrderLinePayment> unrefunded = paid.stream().filter(payment -> {
  584. PosOrderLineRefund refund = refundService.getByPaymentId(payment.getId());
  585. return refund == null || !"REFUNDED".equals(refund.getStatus());
  586. }).toList();
  587. if (Long.valueOf(1L).equals(order.getPayStatus()) && unrefunded.size() == 1) {
  588. return unrefunded.get(0);
  589. }
  590. if (Long.valueOf(2L).equals(order.getPayStatus()) && !paid.isEmpty()) {
  591. return paid.stream().filter(payment -> {
  592. PosOrderLineRefund refund = refundService.getByPaymentId(payment.getId());
  593. return refund != null && "REFUNDED".equals(refund.getStatus());
  594. }).findFirst().orElse(paid.get(0));
  595. }
  596. if (unrefunded.size() > 1) {
  597. PosOrderLinePayment manual = unrefunded.get(0);
  598. manual.setStatus("MANUAL_REVIEW");
  599. return manual;
  600. }
  601. if (paid.size() == 1) {
  602. return paid.get(0);
  603. }
  604. return attempts.stream().filter(payment -> payment.getActiveDdId() != null).findFirst()
  605. .orElseGet(() -> attempts.stream().max(Comparator
  606. .comparing(PosOrderLinePayment::getCreateTime,
  607. Comparator.nullsFirst(Date::compareTo))
  608. .thenComparing(PosOrderLinePayment::getId,
  609. Comparator.nullsFirst(Long::compareTo))).orElse(null));
  610. }
  611. private static LinePayCredential toCredential(PosStoreLinePay credential) {
  612. return new LinePayCredential(credential.getId(), credential.getChannelId(),
  613. credential.getChannelSecret());
  614. }
  615. private RetrieveResult retrieveWithCredentialRecovery(PosOrderLinePayment payment, String source,
  616. PosStoreLinePay originalVersion,
  617. LinePayCredential originalCredential)
  618. throws Exception {
  619. LinePayResponse original = retrieve(payment, source, originalCredential);
  620. if (!isCredentialAuthFailure(original)) {
  621. return new RetrieveResult(original, originalCredential);
  622. }
  623. PosStoreLinePay current = credentialService.getCurrent(payment.getStoreId());
  624. if (current == null || Objects.equals(current.getId(), originalVersion.getId())
  625. || !Objects.equals(current.getChannelId(), originalVersion.getChannelId())
  626. || !Objects.equals(current.getEnvironment(), originalVersion.getEnvironment())) {
  627. return new RetrieveResult(original, originalCredential);
  628. }
  629. LinePayCredential currentCredential = toCredential(current);
  630. LinePayResponse proof = retrieve(payment, source, currentCredential);
  631. return capturedPayment(proof, payment) == null
  632. ? new RetrieveResult(original, originalCredential)
  633. : new RetrieveResult(proof, currentCredential);
  634. }
  635. private LinePayResponse retrieve(PosOrderLinePayment payment, String source,
  636. LinePayCredential credential) throws Exception {
  637. return gatewayCall("RETRIEVE", source, payment, credential.id(),
  638. () -> payment.getTransactionId() == null
  639. ? linePayClient.retrieveByOrderId(credential, payment.getLineOrderId())
  640. : linePayClient.retrieveByTransactionId(credential, payment.getTransactionId()));
  641. }
  642. private static boolean isCredentialAuthFailure(LinePayResponse response) {
  643. return response != null && CREDENTIAL_AUTH_FAILURE_CODES.contains(response.returnCode());
  644. }
  645. private boolean isLocallyRefunded(Long paymentId) {
  646. PosOrderLineRefund refund = refundService.getByPaymentId(paymentId);
  647. return refund != null && "REFUNDED".equals(refund.getStatus());
  648. }
  649. private record RetrieveResult(LinePayResponse response, LinePayCredential credential) {
  650. }
  651. private static LinePayCreateResult createResult(PosOrderLinePayment payment, boolean reused) {
  652. LinePayCreateResult result = new LinePayCreateResult();
  653. result.setDdId(payment.getDdId());
  654. result.setPaymentId(payment.getId());
  655. result.setLineOrderId(payment.getLineOrderId());
  656. result.setTransactionId(payment.getTransactionId());
  657. result.setPaymentUrl(payment.getPaymentUrlWeb());
  658. result.setStatus(payment.getStatus());
  659. result.setReusedAttempt(reused);
  660. return result;
  661. }
  662. private static String newLineOrderId() {
  663. return "LP" + UUID.randomUUID().toString().replace("-", "").toUpperCase(Locale.ROOT);
  664. }
  665. private <T> T gatewayCall(String action, String source, PosOrderLinePayment payment,
  666. LinePayGatewayAuditService.GatewayCall<T> call) throws Exception {
  667. return gatewayCall(action, source, payment, payment.getCredentialId(), call);
  668. }
  669. private <T> T gatewayCall(String action, String source, PosOrderLinePayment payment,
  670. Long credentialId,
  671. LinePayGatewayAuditService.GatewayCall<T> call) throws Exception {
  672. if (auditService == null) {
  673. return call.call();
  674. }
  675. return auditService.execute(action, source == null ? "LINE" : source,
  676. payment.getId(), null, credentialId, payment.getStoreId(),
  677. payment.getDdId(), payment.getLineOrderId(), payment.getTransactionId(), call);
  678. }
  679. private static Date shortly() {
  680. return new Date(System.currentTimeMillis() + 30_000L);
  681. }
  682. private Date authDeadline() {
  683. return new Date(System.currentTimeMillis() + authDeadlineMinutes * 60L * 1000L);
  684. }
  685. private Date unknownDeadline() {
  686. return new Date(System.currentTimeMillis() + unknownDeadlineHours * 60L * 60L * 1000L);
  687. }
  688. }