LinePayService.java 33 KB

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