주문 결제 개선 및 결제취소 조회 인증 만료 처리
- 일반·구독 주문서(PC/모바일) 통화 라벨 표기, 통화 전환 시 첫 결제수단 자동 선택 - Stripe 이메일 자동 입력, 복귀 주소 설정 우선 적용, 진입 이력 GET 처리, 취소 안내·결제결과 회전 표시 - 일반 Stripe 상품명 전체 병 수 요약, 구독 주문은 구독서비스명을 PG에 전달 - 재고 부족 시 상품명·주문 수량·구매 가능 수량 안내 - 결제취소 조회 인증 만료 안내·로그인 이동, 조회 GET 전환, 중복 리다이렉트 종료 - 관련 Java·JS 테스트 추가 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -282,6 +282,9 @@ public class BOMaMtngService {
|
||||
int useMlgAmt = Integer.parseInt(goodsList.get(0).getUseMlgAmt());
|
||||
int useCashMlgAmt = Integer.parseInt(goodsList.get(0).getUseCashMlgAmt());
|
||||
Set<String> validKeys = new HashSet<>(); // 변경 구매정보 리스트
|
||||
InventoryShortageMessage inventoryMessage = new InventoryShortageMessage();
|
||||
Set<String> validationKeys = Arrays.stream(PurchsValidCode.values())
|
||||
.map(PurchsValidCode::getCode).collect(Collectors.toSet());
|
||||
|
||||
//마일리지 잔액 확인
|
||||
BOMlgSrchVO mlgVo = new BOMlgSrchVO();
|
||||
@@ -317,12 +320,14 @@ public class BOMaMtngService {
|
||||
// 값이 1인 항목 == 변경항목
|
||||
List<String> rs = resultValid.stream()
|
||||
.flatMap(map -> map.entrySet().stream())
|
||||
.filter(entry -> validationKeys.contains(entry.getKey()))
|
||||
.filter(entry -> "1".equals(StringUtil.isNullToString(entry.getValue())))
|
||||
.map(entry -> StringUtil.isNullToString(entry.getKey()))
|
||||
.toList();
|
||||
|
||||
//구매정보 변경유형 저장
|
||||
validKeys.addAll(rs);
|
||||
inventoryMessage.add(goods, resultValid);
|
||||
|
||||
}
|
||||
|
||||
@@ -338,7 +343,9 @@ public class BOMaMtngService {
|
||||
StringBuilder returnMsg = new StringBuilder();
|
||||
|
||||
//상품구매수량제한걸렸을 경우
|
||||
if(validKeys.contains("purchsQyLmtt")){
|
||||
if (inventoryMessage.hasShortages()) {
|
||||
returnMsg.append(inventoryMessage.render());
|
||||
}else if(validKeys.contains("purchsQyLmtt")){
|
||||
|
||||
returnMsg.append("구매수량 제한 상품입니다.");
|
||||
returnMsg.append(System.lineSeparator()); //줄바꿈
|
||||
@@ -519,6 +526,8 @@ public class BOMaMtngService {
|
||||
BOPreOrderValidVO.builder()
|
||||
.sopmalCd(StringUtil.isNullToString(goods.get("sopmalCd")))
|
||||
.goodsInnb(StringUtil.isNullToString(goods.get("goodsInnb")))
|
||||
.goodsNm(StringUtil.isNullToString(goods.get("goodsNm")))
|
||||
.optionNm(StringUtil.isNullToString(goods.get("option")))
|
||||
.goodsOptnInnb(StringUtil.isNullToString(goods.get("goodsOptnInnb")))
|
||||
.subsvcInnb(StringUtil.isNullToString(goods.get("subsvcInnb")))
|
||||
.mbrInnb(StringUtil.isNullToString(goods.get("mbrInnb")))
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.wb.domainserver.domain.admin.instcSle.mtng.service;
|
||||
|
||||
import com.wb.domainserver.domain.admin.instcSle.mtng.vo.BOPreOrderValidVO;
|
||||
import com.wb.domainserver.util.dataaccess.DataMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** 재고 판정은 변경하지 않고 조회된 재고 수량으로 안내문만 구성한다. */
|
||||
final class InventoryShortageMessage {
|
||||
private final Map<String, Goods> goodsById = new LinkedHashMap<>();
|
||||
|
||||
void add(BOPreOrderValidVO request, List<DataMap> rows) {
|
||||
if (rows.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
boolean subscription = StringUtils.hasText(request.getSubsvcInnb());
|
||||
String key = request.getSopmalCd() + ":" + (subscription
|
||||
? "subscription:" + request.getSubsvcInnb() : "goods:" + request.getGoodsInnb());
|
||||
Goods goods = goodsById.computeIfAbsent(key, ignored -> new Goods());
|
||||
goods.name = StringUtils.hasText(request.getGoodsNm()) ? request.getGoodsNm()
|
||||
: "상품번호 " + request.getGoodsInnb();
|
||||
goods.requested = request.getGoodsTotalQy();
|
||||
goods.unit = "SK".equals(request.getSopmalCd()) ? "병" : "개";
|
||||
String optionKey = subscription ? request.getSubsvcInnb() : request.getGoodsOptnInnb();
|
||||
Option option = goods.options.computeIfAbsent(optionKey, ignored -> new Option());
|
||||
option.name = StringUtils.hasText(request.getOptionNm()) ? request.getOptionNm()
|
||||
: "옵션번호 " + request.getGoodsOptnInnb();
|
||||
option.requested += Long.parseLong(request.getQy());
|
||||
for (DataMap row : rows) {
|
||||
if (row.get("stockGoodsNm") != null && StringUtils.hasText(row.get("stockGoodsNm").toString())) {
|
||||
goods.name = String.valueOf(row.get("stockGoodsNm"));
|
||||
}
|
||||
goods.shortage |= "1".equals(String.valueOf(row.get("invntry")))
|
||||
|| "1".equals(String.valueOf(row.get("invntryPolicy")));
|
||||
option.available = minimum(option.available, row.get("optionAvailableQy"));
|
||||
goods.available = minimum(goods.available, row.get("goodsAvailableQy"));
|
||||
}
|
||||
}
|
||||
|
||||
boolean hasShortages() {
|
||||
return goodsById.values().stream().anyMatch(goods -> goods.shortage);
|
||||
}
|
||||
|
||||
String render() {
|
||||
StringBuilder message = new StringBuilder("다음 상품의 구매 가능한 재고가 부족합니다.");
|
||||
boolean sharedLimit = false;
|
||||
for (Goods goods : goodsById.values()) {
|
||||
if (!goods.shortage) {
|
||||
continue;
|
||||
}
|
||||
message.append("\n\n· ").append(goods.name);
|
||||
if (goods.options.size() == 1) {
|
||||
Option option = goods.options.values().iterator().next();
|
||||
message.append("\n 주문 수량: ").append(goods.requested)
|
||||
.append(goods.unit).append(" / 현재 구매 가능: ")
|
||||
.append(quantity(minimum(option.available, goods.available), goods.unit));
|
||||
} else {
|
||||
sharedLimit = true;
|
||||
message.append("\n 총 주문 수량: ").append(goods.requested)
|
||||
.append(goods.unit).append(" / 상품 전체 재고 한도: ").append(quantity(goods.available, goods.unit));
|
||||
for (Option option : goods.options.values()) {
|
||||
message.append("\n - ").append(option.name).append(": 주문 ")
|
||||
.append(option.requested).append(goods.unit).append(" / 옵션별 구매 가능 ")
|
||||
.append(quantity(minimum(option.available, goods.available), goods.unit));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sharedLimit) {
|
||||
message.append("\n\n옵션별 가능 수량은 상품 전체 재고 한도 안에서 함께 적용됩니다.");
|
||||
}
|
||||
return message.append("\n\n주문 수량을 줄이거나 해당 상품을 제외한 후 다시 주문해 주세요.").toString();
|
||||
}
|
||||
|
||||
private static Long minimum(Long current, Object value) {
|
||||
if (value == null) {
|
||||
return current;
|
||||
}
|
||||
long quantity = Math.max(0L, Long.parseLong(value.toString()));
|
||||
return current == null ? quantity : Math.min(current, quantity);
|
||||
}
|
||||
|
||||
private static String quantity(Long value, String unit) {
|
||||
return value == null ? "확인 불가" : value + unit;
|
||||
}
|
||||
|
||||
private static final class Goods {
|
||||
String name;
|
||||
String unit;
|
||||
long requested;
|
||||
Long available;
|
||||
boolean shortage;
|
||||
final Map<String, Option> options = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
private static final class Option {
|
||||
String name;
|
||||
long requested;
|
||||
Long available;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,10 @@ public class BOPreOrderValidVO {
|
||||
/*상품고유번호*/
|
||||
private String goodsInnb;
|
||||
|
||||
private String goodsNm;
|
||||
|
||||
private String optionNm;
|
||||
|
||||
/*상품옵션고유번호*/
|
||||
private String goodsOptnInnb;
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import com.wb.domainserver.domain.gcone.bbs.cnslt.vo.BOCnsltVO;
|
||||
import com.wb.domainserver.domain.gcone.common.service.BOCommonService;
|
||||
import com.wb.domainserver.domain.gcone.goods.purchs.mapper.BOPurchsMapper;
|
||||
import com.wb.domainserver.domain.gcone.goods.purchs.vo.*;
|
||||
import com.wb.domainserver.domain.gcone.goods.subsvc.service.BOSubsvcService;
|
||||
import com.wb.domainserver.domain.gcone.goods.subsvc.vo.BOSubsvcVO;
|
||||
import com.wb.domainserver.domain.gcone.ordr.ordr.mapper.BOOrdrMapper;
|
||||
import com.wb.domainserver.domain.gcone.ordr.ordr.vo.BOOrderRestAmtVO;
|
||||
import com.wb.domainserver.exception.WnbtRestException;
|
||||
@@ -51,6 +53,9 @@ public class BOOrderService {
|
||||
@Autowired
|
||||
private BOOrderNoService orderNoService;
|
||||
|
||||
@Autowired
|
||||
private BOSubsvcService subsvcService;
|
||||
|
||||
/**
|
||||
* 관리자 주문 > 주문 등록
|
||||
* @param param
|
||||
@@ -976,6 +981,10 @@ public class BOOrderService {
|
||||
***************************************** */
|
||||
BOOrderSetleVO setleVO = insertSetle(orderVO, setle, insertedGoodsList, enuri, purchsr); // 결제 정보 등록
|
||||
|
||||
if (orderVO.getSubsvcInnb() == null && "stripe".equals(setleVO.getPgCmpnyNm())) {
|
||||
setleVO.setGoodsNm(buildStripeGoodsName(insertedGoodsList.get(0).getGoodsNm(), goodsList));
|
||||
}
|
||||
|
||||
// 결제금액 원화로 변경
|
||||
// setleVO.setSetleAmt(setleVO.getWonSetleAmt());
|
||||
|
||||
@@ -987,6 +996,13 @@ public class BOOrderService {
|
||||
return content;
|
||||
}
|
||||
|
||||
static String buildStripeGoodsName(String firstGoodsName, List<LinkedHashMap> goodsList) {
|
||||
long totalQuantity = goodsList.stream()
|
||||
.mapToLong(goods -> ((Number) goods.get("qy")).longValue())
|
||||
.sum();
|
||||
return totalQuantity > 1 ? firstGoodsName + " 외 " + (totalQuantity - 1) + "병" : firstGoodsName;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void insertCnsltMemo(String sopmalCd, BigInteger ordrInnb, LinkedHashMap purchsr, LinkedHashMap enuri, AnonymousUser user) {
|
||||
try{
|
||||
@@ -1231,7 +1247,16 @@ public class BOOrderService {
|
||||
|
||||
String goodsNm = "";
|
||||
|
||||
if (insertedGoodsList.size() > 1) {
|
||||
if (orderVO.getSubsvcInnb() != null) {
|
||||
BOSubsvcVO subscription = new BOSubsvcVO();
|
||||
subscription.setSopmalCd(sopmalCd);
|
||||
subscription.setSubsvcInnb(orderVO.getSubsvcInnb().toString());
|
||||
DataMap detail = subsvcService.selectBOSubsvcDetail(subscription);
|
||||
goodsNm = detail == null ? "" : StringUtil.isNullToString(detail.get("subsvcNm"));
|
||||
if (!StringUtils.hasText(goodsNm)) {
|
||||
throw new WnbtRestException("구독서비스명을 확인할 수 없습니다. 다시 시도해 주세요.");
|
||||
}
|
||||
} else if (insertedGoodsList.size() > 1) {
|
||||
goodsNm = insertedGoodsList.get(0).getGoodsNm() + " 외" + (insertedGoodsList.size() - 1);
|
||||
} else {
|
||||
goodsNm = insertedGoodsList.get(0).getGoodsNm();
|
||||
|
||||
@@ -153,6 +153,8 @@ public class LoginController {
|
||||
CookieUtils.deleteCookie("token_expire", "/", request, response);
|
||||
|
||||
response.sendRedirect("/sopmal/login/page/" + RealmContextHolder.getRealm() + "?redirect_uri=" + URLEncoder.encode(request.getParameter("redirect_uri"), StandardCharsets.UTF_8));
|
||||
// 로그인 이동을 확정한 뒤 원래 URL로 다시 리다이렉트하지 않는다.
|
||||
return;
|
||||
|
||||
}else{
|
||||
//리프레쉬 토큰 있을 경우(존재한다는 자체가 refresh토큰이 만료된 상태가 아님)
|
||||
|
||||
@@ -23,13 +23,18 @@ import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -47,6 +52,14 @@ public class StripeController {
|
||||
|
||||
private Util utilClass;
|
||||
|
||||
@Value("${stripe.checkout.return-base-url:}")
|
||||
private String stripeReturnBaseUrl = "";
|
||||
|
||||
String resolveStripeReturnBaseUrl(String shopDomain) {
|
||||
String baseUrl = stripeReturnBaseUrl.isBlank() ? shopDomain : stripeReturnBaseUrl.trim();
|
||||
return baseUrl.replaceAll("/+$", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* stripe 결제 Url 호출
|
||||
* @return selectStripeTest.jsp
|
||||
@@ -57,8 +70,7 @@ public class StripeController {
|
||||
BOMaSopmalVO sopmalVO = new BOMaSopmalVO();
|
||||
sopmalVO.setSopmalCd(request.getParameter("sopmalCd"));
|
||||
DataMap sopmalInfo = boMaSopmalService.selectBOSopmalUpdtForm(sopmalVO).get(0);
|
||||
String domnAdres = sopmalInfo.get("pcDomnAdres").toString();
|
||||
// domnAdres = "http://loc.soju1sake1.com:18500";
|
||||
String domnAdres = resolveStripeReturnBaseUrl(sopmalInfo.get("pcDomnAdres").toString());
|
||||
|
||||
StripeClient stripeClient = payService.createStripeClient(request.getParameter("sopmalCd"));
|
||||
SessionCreateParams.Builder paramsBuilder = SessionCreateParams.builder()
|
||||
@@ -71,6 +83,11 @@ public class StripeController {
|
||||
.build()
|
||||
);
|
||||
|
||||
String customerEmail = request.getParameter("ordrrEmail");
|
||||
if (customerEmail != null && !customerEmail.isBlank()) {
|
||||
paramsBuilder.setCustomerEmail(customerEmail.trim());
|
||||
}
|
||||
|
||||
paramsBuilder.addLineItem(
|
||||
SessionCreateParams.LineItem.builder()
|
||||
.setQuantity(1L)
|
||||
@@ -149,6 +166,10 @@ public class StripeController {
|
||||
|
||||
DataMap result = new DataMap();
|
||||
result.put("checkoutUrl", session.getUrl());
|
||||
result.put("redirectUrl", UriComponentsBuilder.fromPath(request.getContextPath() + "/sopmal/stripe/checkoutRedirect")
|
||||
.queryParam("sessionId", session.getId())
|
||||
.queryParam("sopmalCd", request.getParameter("sopmalCd"))
|
||||
.build().encode().toUriString());
|
||||
|
||||
WnbtResponseBody body = new WnbtResponseBody(result);
|
||||
WnbtResponse response = WnbtResponse.builder().resBody(body).build();
|
||||
@@ -156,6 +177,23 @@ public class StripeController {
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/** Stripe 진입 이력을 GET으로 남겨 뒤로가기 시 주문 POST를 재전송하지 않는다. */
|
||||
@Policy(AuthPolicy.RESTRICTED)
|
||||
@GetMapping("/checkoutRedirect")
|
||||
public String checkoutRedirect(ModelMap model, @RequestParam("sessionId") String sessionId,
|
||||
@RequestParam("sopmalCd") String sopmalCd) throws StripeException {
|
||||
Session session = payService.createStripeClient(sopmalCd).v1().checkout().sessions().retrieve(sessionId);
|
||||
AnonymousUser user = MemberJwtContextHolder.getMemberInfo();
|
||||
if (!String.valueOf(user.getUserInnb()).equals(session.getMetadata().get("mbrInnb"))
|
||||
|| !sopmalCd.equals(session.getMetadata().get("sopmalCd"))) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
|
||||
}
|
||||
model.addAttribute("checkoutUrl", "open".equals(session.getStatus()) ? session.getUrl() : "");
|
||||
model.addAttribute("sessionId", sessionId);
|
||||
model.addAttribute("sopmalCd", sopmalCd);
|
||||
return "sopmal/ordr/StripeRedirectR";
|
||||
}
|
||||
|
||||
@Policy(AuthPolicy.RESTRICTED)
|
||||
@RequestMapping(value = "/stripeSuccess")
|
||||
public String stripeSuccess(ModelMap model, @RequestParam("sessionId") String sessionId, @RequestParam("sopmalCd") String sopmalCd) throws StripeException {
|
||||
@@ -214,12 +252,16 @@ public class StripeController {
|
||||
|
||||
@Policy(AuthPolicy.RESTRICTED)
|
||||
@RequestMapping(value = "/stripeCancel")
|
||||
public String stripeCancel(ModelMap model, @RequestParam("sessionId") String sessionId, @RequestParam("sopmalCd") String sopmalCd) throws StripeException {
|
||||
public String stripeCancel(HttpServletRequest request, ModelMap model, @RequestParam("sessionId") String sessionId, @RequestParam("sopmalCd") String sopmalCd) throws StripeException {
|
||||
StripeClient stripeClient = payService.createStripeClient(sopmalCd);
|
||||
Session session = stripeClient.v1().checkout().sessions().retrieve(sessionId);
|
||||
|
||||
String status = session.getStatus();
|
||||
String paymentStatus = session.getPaymentStatus();
|
||||
if (!("complete".equals(status) && "paid".equals(paymentStatus))) {
|
||||
model.addAttribute("mobile", "sopmal_m".equals(Util.getDeviceType(request)));
|
||||
return "sopmal/ordr/StripeCancelR";
|
||||
}
|
||||
JsonObject customerDetails = null;
|
||||
JsonObject rawJson = session.getRawJsonObject();
|
||||
if (rawJson != null
|
||||
@@ -252,6 +294,6 @@ public class StripeController {
|
||||
model.addAttribute("sesionId", session.getMetadata().get("sesionId"));
|
||||
model.addAttribute("subsvcInnb", session.getMetadata().get("subsvcInnb"));
|
||||
|
||||
return "sopmal/ordr/PayStripeCnclC";
|
||||
return "sopmal/ordr/PayStripeSucsC";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,6 +402,9 @@
|
||||
WHEN (IFNULL(GOODS.REST_INVNTRY_QY, 0) - IFNULL(GOODS.AVLINV_LMTT_QY, 0)) >= #{goodsTotalQy} THEN 0
|
||||
ELSE 1 END
|
||||
END AS INVNTRY_POLICY -- 상품별 옵션 수량합산 재고유무
|
||||
, MAX(GOODS.GOODS_NM) AS STOCK_GOODS_NM
|
||||
, MIN(GREATEST(0, IFNULL(OPTN.INVNTRY_QY, 0) - IFNULL(OPTN.USEFUL_INVNTRY_QY, 0))) AS OPTION_AVAILABLE_QY
|
||||
, MIN(GREATEST(0, IFNULL(GOODS.REST_INVNTRY_QY, 0) - IFNULL(GOODS.AVLINV_LMTT_QY, 0))) AS GOODS_AVAILABLE_QY
|
||||
, CASE WHEN IFNULL(OPTN.SLEPRIC, 0) = #{slepric} THEN 0 ELSE 1 END AS SELPRIC -- 판매가 변경여부
|
||||
, CASE
|
||||
WHEN OPTN.COMPNB_DSCNT_AT = #{compnbDscntAt} AND OPTN.SCTN_DSCNT_AT = #{sctnDscntAt} THEN 0
|
||||
@@ -430,6 +433,7 @@
|
||||
, IFNULL(EVT_DUPL_COUPON.DPLCT_LMTT_COUPON, 0) AS DPLCT_LMTT_COUPON -- 이벤트 중복제한 쿠폰 존재여부
|
||||
, CASE WHEN OPTN.OPTN_EXPSR_AT = 'Y' THEN 0 ELSE 1 END AS OPTN_EXPSR_AT
|
||||
FROM (SELECT GG.GOODS_INNB -- 상품 고유번호
|
||||
, GG.GOODS_NM
|
||||
, GG.SOPMAL_CD -- 쇼핑몰코드
|
||||
, GG.DLVY_GROUP_INNB -- 배송그룹고유번호
|
||||
, GG.GOODS_KND_CD -- 상품구분 01 상품
|
||||
@@ -801,6 +805,9 @@
|
||||
ELSE 1
|
||||
END
|
||||
END AS INVNTRY_POLICY, -- 상품별 옵션 수량합산 재고유무
|
||||
MAX(GOODS.GOODS_NM) AS STOCK_GOODS_NM,
|
||||
MIN(GREATEST(0, GOODS.INVNTRY_QY - IFNULL(GOODS.USEFUL_INVNTRY_QY, 0))) AS OPTION_AVAILABLE_QY,
|
||||
MIN(GREATEST(0, IFNULL(GOODS.REST_INVNTRY_QY, 0) - IFNULL(GOODS.AVLINV_LMTT_QY, 0))) AS GOODS_AVAILABLE_QY,
|
||||
CASE
|
||||
WHEN IFNULL(GOODS.SUBSVC_CT, 0) = #{slepric} THEN 0
|
||||
ELSE 1
|
||||
@@ -817,6 +824,7 @@
|
||||
FROM (
|
||||
SELECT
|
||||
GS.SUBSVC_INNB, -- 구독서비스 고유번호
|
||||
GG.GOODS_NM,
|
||||
GG.GOODS_INNB, -- 상품 고유번호
|
||||
GS.SOPMAL_CD, -- 쇼핑몰코드
|
||||
GS.SUBSVC_CT, -- 구독료
|
||||
|
||||
46
src/main/resources/static/js/sopmal/refund-popup.js
Normal file
46
src/main/resources/static/js/sopmal/refund-popup.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/* 결제취소 화면 조회 전용. 실제 취소/환불 요청은 처리하지 않는다. */
|
||||
async function fn_load_refund_popup(url, data, onReady) {
|
||||
const requestUrl = new URL(url, window.location.href);
|
||||
Object.entries(data).forEach(([key, value]) => requestUrl.searchParams.set(key, value));
|
||||
|
||||
const loginRequired = function () {
|
||||
window.alert('로그인 시간이 만료되었습니다. 다시 로그인해 주세요.');
|
||||
const loginUrl = new URL('/sopmal/login', window.location.href);
|
||||
// 로그인 후 AJAX 팝업 주소가 아닌 원래 주문 상세로 돌아온다.
|
||||
loginUrl.searchParams.set('redirect_uri', window.location.href);
|
||||
window.location.assign(loginUrl.href);
|
||||
};
|
||||
|
||||
try {
|
||||
// 갱신 후 리다이렉트에서도 주문 조회 조건이 유지되도록 GET으로 요청한다.
|
||||
const response = await fetch(requestUrl.href, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
cache: 'no-store',
|
||||
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
||||
});
|
||||
const finalUrl = new URL(response.url || requestUrl.href, window.location.href);
|
||||
if (response.status === 401 || (response.redirected &&
|
||||
/^\/sopmal\/(login(?:\/|$)|logout(?:\/|$)|main(?:\/|$))/.test(finalUrl.pathname))) {
|
||||
loginRequired();
|
||||
return;
|
||||
}
|
||||
if (!response.ok || finalUrl.origin !== requestUrl.origin || finalUrl.pathname !== requestUrl.pathname) {
|
||||
throw new Error('Unexpected refund popup response');
|
||||
}
|
||||
|
||||
// 로그인 HTML이나 빈 응답은 화면에 넣지 않는다. 반환된 스크립트도 실행하지 않는다.
|
||||
const html = await response.text();
|
||||
const parsed = new DOMParser().parseFromString(html, 'text/html');
|
||||
const popup = parsed.querySelector('#refund-pop');
|
||||
if (!popup) {
|
||||
throw new Error('Missing refund popup');
|
||||
}
|
||||
popup.querySelectorAll('script').forEach(script => script.remove());
|
||||
$('#refund-pop').remove();
|
||||
$('#refundPopupDiv').empty().append(popup);
|
||||
onReady($(popup));
|
||||
} catch (error) {
|
||||
window.alert('결제취소 화면을 불러오지 못했습니다. 잠시 후 다시 시도해 주세요.');
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@
|
||||
<c:set var="order" value="${data.order}"/>
|
||||
|
||||
<script src="<c:url value='/js/dlvylc/dlvylc.js'/>"></script>
|
||||
<script src="<c:url value='/js/sopmal/refund-popup.js'/>"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
@@ -664,29 +665,18 @@
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type:'post',
|
||||
url:'<c:url value="/sopmal/mpage/ordr/selectSetleCanclPopupForm"/>',
|
||||
data:{
|
||||
'sopmalCd':sopmalCd,
|
||||
'ordrInnb':ordrInnb,
|
||||
'ordrDlvyInnb':ordrDlvyInnb
|
||||
},
|
||||
success:function(html) {
|
||||
$('#refund-pop').remove();
|
||||
fn_load_refund_popup('<c:url value="/sopmal/mpage/ordr/selectSetleCanclPopupForm"/>', {
|
||||
'sopmalCd':sopmalCd,
|
||||
'ordrInnb':ordrInnb,
|
||||
'ordrDlvyInnb':ordrDlvyInnb
|
||||
}, function(popup) {
|
||||
$(popup).find('.orderRefundPop .select-box select').niceSelect();
|
||||
|
||||
$('#refundPopupDiv').html(html);
|
||||
$(popup).find('#refundAcntnb').on("input", function () {
|
||||
this.value = this.value.replace(/[^0-9]/g, "");
|
||||
});
|
||||
|
||||
const popup = $('#refund-pop');
|
||||
|
||||
$(popup).find('.orderRefundPop .select-box select').niceSelect();
|
||||
|
||||
$(popup).find('#refundAcntnb').on("input", function () {
|
||||
this.value = this.value.replace(/[^0-9]/g, "");
|
||||
});
|
||||
|
||||
$(popup).modal();
|
||||
}
|
||||
$(popup).modal();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<jsp:include page="/WEB-INF/views/comm/Include.jsp"/>
|
||||
|
||||
<script src="<c:url value='/js/dlvylc/dlvylc.js'/>"></script>
|
||||
<script src="<c:url value='/js/sopmal/refund-popup.js'/>"></script>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
@@ -659,29 +660,18 @@
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
type:'post',
|
||||
url:'<c:url value="/sopmal/mpage/ordr/selectSetleCanclPopupForm"/>',
|
||||
data:{
|
||||
'sopmalCd':sopmalCd,
|
||||
'ordrInnb':ordrInnb,
|
||||
'ordrDlvyInnb':ordrDlvyInnb
|
||||
},
|
||||
success:function(html) {
|
||||
$('#refund-pop').remove();
|
||||
fn_load_refund_popup('<c:url value="/sopmal/mpage/ordr/selectSetleCanclPopupForm"/>', {
|
||||
'sopmalCd':sopmalCd,
|
||||
'ordrInnb':ordrInnb,
|
||||
'ordrDlvyInnb':ordrDlvyInnb
|
||||
}, function(popup) {
|
||||
$(popup).find('.orderRefundPop .select-box select').niceSelect();
|
||||
|
||||
$('#refundPopupDiv').html(html);
|
||||
$(popup).find('#refundAcntnb').on("input", function () {
|
||||
this.value = this.value.replace(/[^0-9]/g, "");
|
||||
});
|
||||
|
||||
const popup = $('#refund-pop');
|
||||
|
||||
$(popup).find('.orderRefundPop .select-box select').niceSelect();
|
||||
|
||||
$(popup).find('#refundAcntnb').on("input", function () {
|
||||
this.value = this.value.replace(/[^0-9]/g, "");
|
||||
});
|
||||
|
||||
$(popup).modal();
|
||||
}
|
||||
$(popup).modal();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -521,7 +521,7 @@
|
||||
});
|
||||
|
||||
/* 결제정보 결제수단 버튼 선택 이벤트 */
|
||||
$('input[name="pg"]').on('click', function() {
|
||||
$('input[name="pg"]').on('click', function(event, skipDefaultMethod) {
|
||||
const form = $(this).closest('#setleForm')
|
||||
const id = $(this).attr('id');
|
||||
const pgCmpnyNm = $(this).val();
|
||||
@@ -582,6 +582,14 @@
|
||||
$('.order-method-normal__btn').removeClass('active');
|
||||
$('.order-method-normal__content').hide();
|
||||
}
|
||||
|
||||
// 통화를 선택하면 사용 가능한 첫 결제수단을 기존 클릭 처리로 선택한다.
|
||||
// 결제수단 버튼에서 라디오를 갱신할 때는 재귀 선택을 생략한다.
|
||||
if (!skipDefaultMethod && ['st01', 'pg03', 'pg04', 'pg06'].includes(id)) {
|
||||
$(form).find('#' + id + 'Content')
|
||||
.find('.order-method-normal__btn:visible:not(:disabled)')
|
||||
.first().trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
/* 결제정보 > 일반결제 > 결제수단 버튼 선택 이벤트 */
|
||||
@@ -590,11 +598,8 @@
|
||||
const content = $('.order-method-normal__content');
|
||||
const useCashMlgAmt = $('#useCashMlgAmt').val();
|
||||
|
||||
//$('#setleSetupTyCd').val() === '01' ? $('#pg04').click() : $('#pg03').click();
|
||||
if ($('#st01').is(':checked')){$('#st01').click();}
|
||||
else if($('#setleSetupTyCd').val() === '01'){$('#pg04').click();}
|
||||
else if($('#setleSetupTyCd').val() === '02'){$('#pg03').click();}
|
||||
else if($('#setleSetupTyCd').val() === '03'){$('#pg06').click();}
|
||||
const pgId = $(this).closest('.pgContent').attr('id').replace(/Content$/, '');
|
||||
$('#' + pgId).trigger('click', [true]);
|
||||
|
||||
$('.order-method-normal__btn').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
@@ -4247,7 +4252,7 @@
|
||||
<c:if test="${'Y' eq payInfo.stripeUseAt}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="st01" value="stripe">
|
||||
<label for="st01">엔화 결제</label>
|
||||
<label for="st01">엔화(¥) 결제</label>
|
||||
</div>
|
||||
<div id="st01Content" class="pgContent order-method-normal" <%-- style="display: none"--%>>
|
||||
<div class="order-method-normal__btn-container">
|
||||
@@ -4263,7 +4268,7 @@
|
||||
<c:if test="${'02' eq payInfo.setleSetupTyCd}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="pg03" value="toss">
|
||||
<label for="pg03">일반결제</label>
|
||||
<label for="pg03">원화(₩) 결제</label>
|
||||
</div>
|
||||
<div id="pg03Content" class="pgContent order-method-normal" <%-- style="display: none"--%>>
|
||||
<div class="order-method-normal__btn-container">
|
||||
@@ -4395,7 +4400,7 @@
|
||||
<c:if test="${'01' eq payInfo.setleSetupTyCd}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="pg04" value="kcp">
|
||||
<label for="pg04">일반결제</label>
|
||||
<label for="pg04">원화(₩) 결제</label>
|
||||
</div>
|
||||
<div id="pg04Content" class="pgContent order-method-normal" <%-- style="display: none"--%>>
|
||||
<div class="order-method-normal__btn-container">
|
||||
@@ -4527,7 +4532,7 @@
|
||||
<c:if test="${'03' eq payInfo.setleSetupTyCd}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="pg06" value="kiwoom">
|
||||
<label for="pg06">일반결제</label>
|
||||
<label for="pg06">원화(₩) 결제</label>
|
||||
</div>
|
||||
<div id="pg06Content" class="pgContent order-method-normal" <%-- style="display: none"--%>>
|
||||
<div class="order-method-normal__btn-container">
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
success : function(res) {
|
||||
console.log(res);
|
||||
const data = JSON.parse(res.resBody.result);
|
||||
window.location.href = data.checkoutUrl;
|
||||
window.location.replace(data.redirectUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,9 +18,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<title>결제 성공</title>
|
||||
<title>결제 결과 확인 중</title>
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #f8fafc;
|
||||
color: #202938;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
.payment-progress {
|
||||
padding: 32px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.payment-progress__spinner {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0 auto 24px;
|
||||
border: 4px solid #e2e8f0;
|
||||
border-top-color: #0075d4;
|
||||
border-radius: 50%;
|
||||
animation: payment-spin 0.9s linear infinite;
|
||||
}
|
||||
.payment-progress h1 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 20px;
|
||||
line-height: 1.5;
|
||||
word-break: keep-all;
|
||||
}
|
||||
.payment-progress p {
|
||||
margin: 0;
|
||||
color: #64748b;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@keyframes payment-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.payment-progress__spinner { animation: none; }
|
||||
}
|
||||
</style>
|
||||
<script src="<c:url value='/pc/vendor/jquery-3.7.1.min.js'/>"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
@@ -57,8 +99,10 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<section>
|
||||
<h1>결제 성공</h1>
|
||||
<section class="payment-progress" role="status" aria-live="polite">
|
||||
<div class="payment-progress__spinner" aria-hidden="true"></div>
|
||||
<h1>결제 결과를 확인하고 있습니다.</h1>
|
||||
<p>잠시만 기다려 주세요.</p>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
51
src/main/webapp/WEB-INF/views/sopmal/ordr/StripeCancelR.jsp
Normal file
51
src/main/webapp/WEB-INF/views/sopmal/ordr/StripeCancelR.jsp
Normal file
@@ -0,0 +1,51 @@
|
||||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<c:choose>
|
||||
<c:when test="${mobile}">
|
||||
<jsp:include page="/WEB-INF/views/comm/mobile/Include.jsp"/>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<jsp:include page="/WEB-INF/views/comm/Include.jsp"/>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
<title>결제 중단 안내</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="screen-wrap">
|
||||
<c:choose>
|
||||
<c:when test="${mobile}">
|
||||
<header class="header header--sub">
|
||||
<jsp:include page="/WEB-INF/views/comm/mobile/Header.jsp"/>
|
||||
</header>
|
||||
<jsp:include page="/WEB-INF/views/comm/mobile/TabBar.jsp"/>
|
||||
</c:when>
|
||||
<c:otherwise>
|
||||
<jsp:include page="/WEB-INF/views/comm/Header.jsp"/>
|
||||
</c:otherwise>
|
||||
</c:choose>
|
||||
<main class="main content-wrap">
|
||||
<div class="sub-wrapper sub-page">
|
||||
<div class="sub-content sub-box">
|
||||
<div class="orderFailed">
|
||||
<div class="orderFailed__inner">
|
||||
<h3 class="orderFailed__title">결제를 중단하셨습니다.</h3>
|
||||
<p class="orderFailed__text">이 결제창에서 결제를 완료하지 않았습니다.<br>주문/배송조회에서 주문 상태를 확인한 뒤 다시 결제해 주세요.</p>
|
||||
</div>
|
||||
<div class="sub-bottom-btn-container fullBtnSection">
|
||||
<a class="btn xl outline" href="<c:url value='/sopmal/mpage/ordr/selectMbrOrdrListInqire'/>">주문/배송조회</a>
|
||||
<a class="btn xl outline" href="<c:url value='/sopmal/main'/>">쇼핑 계속하기</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<c:if test="${!mobile}">
|
||||
<jsp:include page="/WEB-INF/views/comm/Footer.jsp"/>
|
||||
</c:if>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
|
||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
|
||||
<c:url var="cancelUrl" value="/sopmal/stripe/stripeCancel">
|
||||
<c:param name="sessionId" value="${sessionId}"/>
|
||||
<c:param name="sopmalCd" value="${sopmalCd}"/>
|
||||
</c:url>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>결제창으로 이동</title>
|
||||
<script>
|
||||
window.addEventListener('pageshow', function (event) {
|
||||
const checkout = document.getElementById('stripeCheckout');
|
||||
const cancel = document.getElementById('stripeCancel');
|
||||
const navigation = performance.getEntriesByType('navigation')[0];
|
||||
// 일반 복귀와 BFCache 복귀 모두 새 결제 세션을 만들지 않고 처리한다.
|
||||
if (event.persisted || (navigation && navigation.type === 'back_forward')
|
||||
|| (history.state && history.state.stripeRedirected)
|
||||
|| !checkout.getAttribute('href')) {
|
||||
window.location.replace(cancel.href);
|
||||
return;
|
||||
}
|
||||
history.replaceState({stripeRedirected: true}, '', window.location.href);
|
||||
window.location.assign(checkout.href);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<p>결제창으로 이동하고 있습니다.</p>
|
||||
<a id="stripeCheckout" href="<c:out value='${checkoutUrl}'/>">결제창 열기</a>
|
||||
<a id="stripeCancel" href="<c:out value='${cancelUrl}'/>">결제 중단</a>
|
||||
</body>
|
||||
</html>
|
||||
@@ -524,7 +524,7 @@
|
||||
});*/
|
||||
|
||||
/* 결제정보 결제수단 버튼 선택 이벤트 */
|
||||
$('input[name="pg"]').on('click', function() {
|
||||
$('input[name="pg"]').on('click', function(event, skipDefaultMethod) {
|
||||
const form = $(this).closest('#setleForm')
|
||||
const id = $(this).attr('id');
|
||||
const pgCmpnyNm = $(this).val();
|
||||
@@ -585,6 +585,14 @@
|
||||
$('.order-method-normal__btn').removeClass('active');
|
||||
$('.order-method-normal__content').hide();
|
||||
}
|
||||
|
||||
// 통화를 선택하면 사용 가능한 첫 결제수단을 기존 클릭 처리로 선택한다.
|
||||
// 결제수단 버튼에서 라디오를 갱신할 때는 재귀 선택을 생략한다.
|
||||
if (!skipDefaultMethod && ['st01', 'pg03', 'pg04', 'pg06'].includes(id)) {
|
||||
$(form).find('#' + id + 'Content')
|
||||
.find('.order-method-normal__btn:visible:not(:disabled)')
|
||||
.first().trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
/* 결제정보 > 일반결제 > 결제수단 버튼 선택 이벤트 */
|
||||
@@ -593,11 +601,8 @@
|
||||
const content = $('.order-method-normal__content');
|
||||
const useCashMlgAmt = $('#useCashMlgAmt').val();
|
||||
|
||||
//$('#setleSetupTyCd').val() === '01' ? $('#pg04').click() : $('#pg03').click();
|
||||
if ($('#st01').is(':checked')){$('#st01').click();}
|
||||
else if($('#setleSetupTyCd').val() === '01'){$('#pg04').click();}
|
||||
else if($('#setleSetupTyCd').val() === '02'){$('#pg03').click();}
|
||||
else if($('#setleSetupTyCd').val() === '03'){$('#pg06').click();}
|
||||
const pgId = $(this).closest('.pgContent').attr('id').replace(/Content$/, '');
|
||||
$('#' + pgId).trigger('click', [true]);
|
||||
|
||||
$('.order-method-normal__btn').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
@@ -4213,7 +4218,7 @@
|
||||
<c:if test="${'Y' eq payInfo.stripeUseAt}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="st01" value="stripe">
|
||||
<label for="st01">엔화 결제</label>
|
||||
<label for="st01">엔화(¥) 결제</label>
|
||||
</div>
|
||||
<div id="st01Content" class="pgContent order-method-normal" <%-- style="display: none"--%>>
|
||||
<div class="order-method-normal__btn-container">
|
||||
@@ -4229,7 +4234,7 @@
|
||||
<c:if test="${'02' eq payInfo.setleSetupTyCd}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="pg03" value="toss">
|
||||
<label for="pg03">일반결제</label>
|
||||
<label for="pg03">원화(₩) 결제</label>
|
||||
</div>
|
||||
<div id="pg03Content" class="pgContent order-method-normal" <%-- style="display: none"--%>>
|
||||
<div class="order-method-normal__btn-container">
|
||||
@@ -4361,7 +4366,7 @@
|
||||
<c:if test="${'01' eq payInfo.setleSetupTyCd}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="pg04" value="kcp">
|
||||
<label for="pg04">일반결제</label>
|
||||
<label for="pg04">원화(₩) 결제</label>
|
||||
</div>
|
||||
<div id="pg04Content" class="pgContent order-method-normal" <%-- style="display: none"--%>>
|
||||
<div class="order-method-normal__btn-container">
|
||||
@@ -4493,7 +4498,7 @@
|
||||
<c:if test="${'03' eq payInfo.setleSetupTyCd}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="pg06" value="kiwoom">
|
||||
<label for="pg06">일반결제</label>
|
||||
<label for="pg06">원화(₩) 결제</label>
|
||||
</div>
|
||||
<div id="pg06Content" class="pgContent order-method-normal" <%-- style="display: none"--%>>
|
||||
<div class="order-method-normal__btn-container">
|
||||
|
||||
@@ -528,7 +528,8 @@
|
||||
$(form).append($('<input/>', {type: 'hidden', name: 'sopmalCd', id: 'sopmalCd', value: sopmalCd}));
|
||||
$(form).append($('<input/>', {type: 'hidden', name: 'ordrInnb', id: 'ordrInnb', value: ordrInnb}));
|
||||
|
||||
$(form).attr('method', 'post');
|
||||
// 인증 갱신 후에도 조회 조건을 유지한다. 실제 취소 등록은 기존 POST를 사용한다.
|
||||
$(form).attr('method', 'get');
|
||||
$(form).attr('action', '<c:url value="/sopmal/mpage/ordr/selectSetleCanclPopupForm"/>');
|
||||
|
||||
$(form).submit();
|
||||
|
||||
@@ -510,7 +510,8 @@
|
||||
$(form).append($('<input/>', {type: 'hidden', name: 'sopmalCd', id: 'sopmalCd', value: sopmalCd}));
|
||||
$(form).append($('<input/>', {type: 'hidden', name: 'ordrInnb', id: 'ordrInnb', value: ordrInnb}));
|
||||
|
||||
$(form).attr('method', 'post');
|
||||
// 인증 갱신 후에도 조회 조건을 유지한다. 실제 취소 등록은 기존 POST를 사용한다.
|
||||
$(form).attr('method', 'get');
|
||||
$(form).attr('action', '<c:url value="/sopmal/mpage/ordr/selectSetleCanclPopupForm"/>');
|
||||
|
||||
$(form).submit();
|
||||
|
||||
@@ -565,7 +565,7 @@
|
||||
});
|
||||
|
||||
/* 결제정보 결제수단 버튼 선택 이벤트 */
|
||||
$('input[name="pg"]').on('click', function() {
|
||||
$('input[name="pg"]').on('click', function(event, skipDefaultMethod) {
|
||||
const form = $(this).closest('#setleForm')
|
||||
const id = $(this).attr('id');
|
||||
const pgCmpnyNm = $(this).val();
|
||||
@@ -632,6 +632,14 @@
|
||||
$('.order-method-normal__btn').removeClass('active');
|
||||
$('.order-method-normal__content').hide();
|
||||
}
|
||||
|
||||
// 통화를 선택하면 사용 가능한 첫 결제수단을 기존 클릭 처리로 선택한다.
|
||||
// 결제수단 버튼에서 라디오를 갱신할 때는 재귀 선택을 생략한다.
|
||||
if (!skipDefaultMethod && ['st01', 'pg03', 'pg04', 'pg06'].includes(id)) {
|
||||
$(form).find('#' + id + 'Content')
|
||||
.find('.order-method-normal__btn:visible:not(:disabled)')
|
||||
.first().trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
/* 결제정보 > 일반결제 > 결제수단 버튼 선택 이벤트 */
|
||||
@@ -640,11 +648,8 @@
|
||||
const content = $('.order-method-normal__content');
|
||||
const useCashMlgAmt = $('#useCashMlgAmt').val();
|
||||
|
||||
//$('#setleSetupTyCd').val() === '01' ? $('#pg04').click() : $('#pg03').click();
|
||||
if ($('#st01').is(':checked')){$('#st01').click();}
|
||||
else if($('#setleSetupTyCd').val() === '01'){$('#pg04').click();}
|
||||
else if($('#setleSetupTyCd').val() === '02'){$('#pg03').click();}
|
||||
else if($('#setleSetupTyCd').val() === '03'){$('#pg06').click();}
|
||||
const pgId = $(this).closest('.pgContent').attr('id').replace(/Content$/, '');
|
||||
$('#' + pgId).trigger('click', [true]);
|
||||
|
||||
$('.order-method-normal__btn').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
@@ -4330,7 +4335,7 @@
|
||||
<c:if test="${'Y' eq payInfo.stripeUseAt}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="st01" value="stripe">
|
||||
<label for="st01">엔화 결제</label>
|
||||
<label for="st01">엔화(¥) 결제</label>
|
||||
</div>
|
||||
<div id="st01Content" class="pgContent order-method-normal" <%-- style="display: none"--%>>
|
||||
<div class="order-method-normal__btn-container">
|
||||
@@ -4346,7 +4351,7 @@
|
||||
<c:if test="${'02' eq payInfo.setleSetupTyCd}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="pg03" value="toss">
|
||||
<label for="pg03">일반결제</label>
|
||||
<label for="pg03">원화(₩) 결제</label>
|
||||
</div>
|
||||
<div id="pg03Content" class="pgContent order-method-normal">
|
||||
<div class="order-method-normal__btn-container">
|
||||
@@ -4478,7 +4483,7 @@
|
||||
<c:if test="${'01' eq payInfo.setleSetupTyCd}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="pg04" value="kcp">
|
||||
<label for="pg04">일반결제</label>
|
||||
<label for="pg04">원화(₩) 결제</label>
|
||||
</div>
|
||||
<div id="pg04Content" class="pgContent order-method-normal" >
|
||||
<div class="order-method-normal__btn-container">
|
||||
@@ -4610,7 +4615,7 @@
|
||||
<c:if test="${'03' eq payInfo.setleSetupTyCd}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="pg06" value="kiwoom">
|
||||
<label for="pg06">일반결제</label>
|
||||
<label for="pg06">원화(₩) 결제</label>
|
||||
</div>
|
||||
<div id="pg06Content" class="pgContent order-method-normal" >
|
||||
<div class="order-method-normal__btn-container">
|
||||
|
||||
@@ -568,7 +568,7 @@
|
||||
});*/
|
||||
|
||||
/* 결제정보 결제수단 버튼 선택 이벤트 */
|
||||
$('input[name="pg"]').on('click', function() {
|
||||
$('input[name="pg"]').on('click', function(event, skipDefaultMethod) {
|
||||
const form = $(this).closest('#setleForm')
|
||||
const id = $(this).attr('id');
|
||||
const pgCmpnyNm = $(this).val();
|
||||
@@ -635,6 +635,14 @@
|
||||
$('.order-method-normal__btn').removeClass('active');
|
||||
$('.order-method-normal__content').hide();
|
||||
}
|
||||
|
||||
// 통화를 선택하면 사용 가능한 첫 결제수단을 기존 클릭 처리로 선택한다.
|
||||
// 결제수단 버튼에서 라디오를 갱신할 때는 재귀 선택을 생략한다.
|
||||
if (!skipDefaultMethod && ['st01', 'pg03', 'pg04', 'pg06'].includes(id)) {
|
||||
$(form).find('#' + id + 'Content')
|
||||
.find('.order-method-normal__btn:visible:not(:disabled)')
|
||||
.first().trigger('click');
|
||||
}
|
||||
});
|
||||
|
||||
/* 결제정보 > 일반결제 > 결제수단 버튼 선택 이벤트 */
|
||||
@@ -643,11 +651,8 @@
|
||||
const content = $('.order-method-normal__content');
|
||||
const useCashMlgAmt = $('#useCashMlgAmt').val();
|
||||
|
||||
//$('#setleSetupTyCd').val() === '01' ? $('#pg04').click() : $('#pg03').click();
|
||||
if ($('#st01').is(':checked')){$('#st01').click();}
|
||||
else if($('#setleSetupTyCd').val() === '01'){$('#pg04').click();}
|
||||
else if($('#setleSetupTyCd').val() === '02'){$('#pg03').click();}
|
||||
else if($('#setleSetupTyCd').val() === '03'){$('#pg06').click();}
|
||||
const pgId = $(this).closest('.pgContent').attr('id').replace(/Content$/, '');
|
||||
$('#' + pgId).trigger('click', [true]);
|
||||
|
||||
$('.order-method-normal__btn').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
@@ -4319,7 +4324,7 @@
|
||||
<c:if test="${'Y' eq payInfo.stripeUseAt}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="st01" value="stripe">
|
||||
<label for="st01">엔화 결제</label>
|
||||
<label for="st01">엔화(¥) 결제</label>
|
||||
</div>
|
||||
<div id="st01Content" class="pgContent order-method-normal" <%-- style="display: none"--%>>
|
||||
<div class="order-method-normal__btn-container">
|
||||
@@ -4335,7 +4340,7 @@
|
||||
<c:if test="${'02' eq payInfo.setleSetupTyCd}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="pg03" value="toss">
|
||||
<label for="pg03">일반결제</label>
|
||||
<label for="pg03">원화(₩) 결제</label>
|
||||
</div>
|
||||
<div id="pg03Content" class="pgContent order-method-normal">
|
||||
<div class="order-method-normal__btn-container">
|
||||
@@ -4467,7 +4472,7 @@
|
||||
<c:if test="${'01' eq payInfo.setleSetupTyCd}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="pg04" value="kcp">
|
||||
<label for="pg04">일반결제</label>
|
||||
<label for="pg04">원화(₩) 결제</label>
|
||||
</div>
|
||||
<div id="pg04Content" class="pgContent order-method-normal" >
|
||||
<div class="order-method-normal__btn-container">
|
||||
@@ -4599,7 +4604,7 @@
|
||||
<c:if test="${'03' eq payInfo.setleSetupTyCd}">
|
||||
<div class="custom-radio is-medium">
|
||||
<input type="radio" name="pg" id="pg06" value="kiwoom">
|
||||
<label for="pg06">일반결제</label>
|
||||
<label for="pg06">원화(₩) 결제</label>
|
||||
</div>
|
||||
<div id="pg06Content" class="pgContent order-method-normal" >
|
||||
<div class="order-method-normal__btn-container">
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.wb.domainserver.domain.admin.instcSle.mtng.service;
|
||||
|
||||
import com.wb.domainserver.domain.admin.instcSle.mtng.mapper.BOMaMtngMapper;
|
||||
import com.wb.domainserver.domain.admin.instcSle.mtng.vo.BOPreOrderValidVO;
|
||||
import com.wb.domainserver.domain.gcone.mbr.mlg.service.BOMlgService;
|
||||
import com.wb.domainserver.domain.gcone.mbr.mlg.vo.BOMlgSrchVO;
|
||||
import com.wb.domainserver.util.dataaccess.DataMap;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class InventoryShortageMessageTest {
|
||||
private BOPreOrderValidVO goods(String id, String option, int quantity, int total) {
|
||||
return BOPreOrderValidVO.builder().sopmalCd("SK").goodsInnb(id).goodsNm("요청 상품명")
|
||||
.goodsOptnInnb(option).optionNm("옵션 " + option).qy(String.valueOf(quantity))
|
||||
.goodsTotalQy(total).subsvcInnb("").mbrInnb("1").mbrDvCd("SK")
|
||||
.useMlgAmt("0").useCashMlgAmt("0").build();
|
||||
}
|
||||
|
||||
private DataMap row(String name, int optionStock, int totalStock, int optionError, int totalError) {
|
||||
DataMap row = new DataMap();
|
||||
row.put("stockGoodsNm", name);
|
||||
row.put("optionAvailableQy", optionStock);
|
||||
row.put("goodsAvailableQy", totalStock);
|
||||
row.put("invntry", optionError);
|
||||
row.put("invntryPolicy", totalError);
|
||||
return row;
|
||||
}
|
||||
|
||||
@Test
|
||||
void allShortageProductsAppearButAvailableProductsDoNot() {
|
||||
InventoryShortageMessage message = new InventoryShortageMessage();
|
||||
message.add(goods("138", "1", 2, 2), List.of(row("상품 A", 1, 5, 1, 0)));
|
||||
message.add(goods("128", "1", 1, 1), List.of(row("상품 B", 0, 0, 1, 1)));
|
||||
message.add(goods("170", "1", 1, 1), List.of(row("상품 C", 4, 4, 0, 0)));
|
||||
String text = message.render();
|
||||
assertTrue(text.contains("상품 A\n 주문 수량: 2병 / 현재 구매 가능: 1병"));
|
||||
assertTrue(text.contains("상품 B\n 주문 수량: 1병 / 현재 구매 가능: 0병"));
|
||||
assertFalse(text.contains("상품 C"));
|
||||
assertFalse(text.contains("GC25"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateValidationRowsDoNotDuplicateTheProductOrOrderQuantity() {
|
||||
InventoryShortageMessage message = new InventoryShortageMessage();
|
||||
DataMap row = row("상품 A", 0, 0, 1, 1);
|
||||
message.add(goods("138", "1", 2, 2), List.of(row, row));
|
||||
assertEquals(1, message.render().split("· 상품 A", -1).length - 1);
|
||||
assertTrue(message.render().contains("주문 수량: 2병"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleOptionsShowSharedLimitWithoutAddingTheirCapacities() {
|
||||
InventoryShortageMessage message = new InventoryShortageMessage();
|
||||
message.add(goods("138", "1", 2, 4), List.of(row("상품 A", 3, 3, 0, 1)));
|
||||
message.add(goods("138", "2", 2, 4), List.of(row("상품 A", 5, 3, 0, 1)));
|
||||
assertTrue(message.render().contains("총 주문 수량: 4병 / 상품 전체 재고 한도: 3병"));
|
||||
assertTrue(message.render().contains("옵션 1: 주문 2병 / 옵션별 구매 가능 3병"));
|
||||
assertTrue(message.render().contains("옵션 2: 주문 2병 / 옵션별 구매 가능 3병"));
|
||||
assertTrue(message.render().contains("상품 전체 재고 한도 안에서 함께 적용"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void totalLimitAndNegativeStockAreHandled() {
|
||||
InventoryShortageMessage message = new InventoryShortageMessage();
|
||||
message.add(goods("138", "1", 2, 2), List.of(row("상품 A", 8, -2, 0, 1)));
|
||||
assertTrue(message.render().contains("현재 구매 가능: 0병"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stockCountOfOneAndNumericProductNameAreNotValidationFailures() {
|
||||
BOMaMtngService service = serviceWithStockRows(row("1", 1, 1, 0, 0));
|
||||
Map<String, Object> result = service.selectBOPreOrderValidation(List.of(goods("138", "1", 1, 1)));
|
||||
assertEquals("N", result.get("result"));
|
||||
assertFalse(result.containsKey("message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stockFailuresUseReadableMessageThroughTheService() {
|
||||
BOMaMtngService service = serviceWithStockRows(row("상품 A", 0, 0, 1, 1));
|
||||
Map<String, Object> result = service.selectBOPreOrderValidation(List.of(goods("138", "1", 2, 2)));
|
||||
assertEquals("Y", result.get("result"));
|
||||
assertTrue(result.get("message").toString().contains("상품 A\n 주문 수량: 2병 / 현재 구매 가능: 0병"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void inventoryNoticeStillListsProductsWhenAnotherQuantityRestrictionFails() {
|
||||
DataMap row = row("상품 A", 0, 0, 1, 1);
|
||||
row.put("purchsQyLmtt", 1);
|
||||
Map<String, Object> result = serviceWithStockRows(row)
|
||||
.selectBOPreOrderValidation(List.of(goods("138", "1", 2, 2)));
|
||||
assertTrue(result.get("message").toString().contains("현재 구매 가능: 0병"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unrelatedValidationMessageIsPreserved() {
|
||||
DataMap row = row("상품 A", 5, 5, 0, 0);
|
||||
row.put("selpric", 1);
|
||||
Map<String, Object> result = serviceWithStockRows(row)
|
||||
.selectBOPreOrderValidation(List.of(goods("138", "1", 1, 1)));
|
||||
assertEquals("Y", result.get("result"));
|
||||
assertTrue(result.get("message").toString().contains("GC25S0324006"));
|
||||
}
|
||||
|
||||
private BOMaMtngService serviceWithStockRows(DataMap row) {
|
||||
BOMaMtngService service = new BOMaMtngService();
|
||||
BOMaMtngMapper mapper = mock(BOMaMtngMapper.class);
|
||||
when(mapper.selectBOPreOrderValidation(any())).thenReturn(List.of(row));
|
||||
BOMlgService mileage = mock(BOMlgService.class);
|
||||
when(mileage.selectBOMlgBlceSum(any(BOMlgSrchVO.class))).thenReturn(BigDecimal.ZERO);
|
||||
when(mileage.selectBOCashBlce(any())).thenReturn(BigDecimal.ZERO);
|
||||
ReflectionTestUtils.setField(service, "mapper", mapper);
|
||||
ReflectionTestUtils.setField(service, "boMlgService", mileage);
|
||||
return service;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.wb.domainserver.domain.gcone.goods.purchs.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class StripeGoodsNameTest {
|
||||
private LinkedHashMap<String, Object> goods(int quantity) {
|
||||
LinkedHashMap<String, Object> goods = new LinkedHashMap<>();
|
||||
goods.put("qy", quantity);
|
||||
return goods;
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneBottleKeepsItsName() {
|
||||
assertEquals("사케 A", BOOrderService.buildStripeGoodsName("사케 A", List.of(goods(1))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void twoBottlesOfTheSameProductIncludeTheAdditionalBottle() {
|
||||
assertEquals("사케 A 외 1병", BOOrderService.buildStripeGoodsName("사케 A", List.of(goods(2))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleProductsUseTheSumOfQuantitiesRatherThanProductCount() {
|
||||
assertEquals("사케 A 외 4병", BOOrderService.buildStripeGoodsName("사케 A", List.of(goods(2), goods(3))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void separateOptionsOfOneProductAlsoCountAsBottles() {
|
||||
assertEquals("사케 A 외 2병", BOOrderService.buildStripeGoodsName("사케 A", List.of(goods(1), goods(2))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.wb.domainserver.domain.gcone.goods.purchs.service;
|
||||
|
||||
import com.wb.domainserver.domain.gcone.goods.purchs.mapper.BOPurchsMapper;
|
||||
import com.wb.domainserver.domain.gcone.goods.purchs.vo.BOOrderItemVO;
|
||||
import com.wb.domainserver.domain.gcone.goods.purchs.vo.BOOrderSetleVO;
|
||||
import com.wb.domainserver.domain.gcone.goods.purchs.vo.BOOrderVO;
|
||||
import com.wb.domainserver.domain.gcone.goods.subsvc.service.BOSubsvcService;
|
||||
import com.wb.domainserver.exception.WnbtRestException;
|
||||
import com.wb.domainserver.util.dataaccess.DataMap;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class SubscriptionPaymentNameTest {
|
||||
private BOOrderService service;
|
||||
private BOSubsvcService subscriptions;
|
||||
private BOPurchsMapper mapper;
|
||||
private BOOrderVO order;
|
||||
private List<BOOrderItemVO> items;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new BOOrderService();
|
||||
subscriptions = mock(BOSubsvcService.class);
|
||||
mapper = mock(BOPurchsMapper.class);
|
||||
BOOrderNoService numbers = mock(BOOrderNoService.class);
|
||||
when(numbers.generateOrderSetleNo()).thenReturn(BigInteger.ONE);
|
||||
ReflectionTestUtils.setField(service, "subsvcService", subscriptions);
|
||||
ReflectionTestUtils.setField(service, "mapper", mapper);
|
||||
ReflectionTestUtils.setField(service, "orderNoService", numbers);
|
||||
order = new BOOrderVO();
|
||||
order.setSopmalCd("SK");
|
||||
order.setOrdrInnb(BigInteger.TEN);
|
||||
order.setSubsvcInnb(28);
|
||||
BOOrderItemVO item = new BOOrderItemVO();
|
||||
item.setGoodsNm("구독서비스상품");
|
||||
items = List.of(item, item, item, item, item, item);
|
||||
DataMap detail = new DataMap();
|
||||
detail.put("subsvcNm", "구독서비스 출시 기념 이벤트");
|
||||
when(subscriptions.selectBOSubsvcDetail(any())).thenReturn(detail);
|
||||
}
|
||||
|
||||
private BOOrderSetleVO settle(String provider) {
|
||||
LinkedHashMap<String, Object> payment = new LinkedHashMap<>();
|
||||
payment.put("rlSetleAmt", "15000");
|
||||
payment.put("wonRlSetleAmt", "142500");
|
||||
payment.put("pgCmpnyNm", provider);
|
||||
payment.put("setleTyCd", "01");
|
||||
payment.put("ordrDvCd", "01");
|
||||
return service.insertSetle(order, payment, items, new LinkedHashMap<>(), new LinkedHashMap<>());
|
||||
}
|
||||
|
||||
@Test
|
||||
void stripeUsesSubscriptionNameAndPreservesAmounts() {
|
||||
BOOrderSetleVO payment = settle("stripe");
|
||||
assertEquals("구독서비스 출시 기념 이벤트", payment.getGoodsNm());
|
||||
assertEquals(new BigDecimal("15000"), payment.getSetleAmt());
|
||||
assertEquals(new BigDecimal("142500"), payment.getWonSetleAmt());
|
||||
verify(subscriptions).selectBOSubsvcDetail(argThat(vo -> "SK".equals(vo.getSopmalCd())
|
||||
&& "28".equals(vo.getSubsvcInnb())));
|
||||
verify(mapper).insertOrderSetle(argThat(vo -> "구독서비스 출시 기념 이벤트".equals(vo.getGoodsNm())));
|
||||
verify(mapper).insertSales(argThat(vo -> "구독서비스 출시 기념 이벤트".equals(vo.getGoodsNm())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void kiwoomUsesTheSameSubscriptionName() {
|
||||
assertEquals("구독서비스 출시 기념 이벤트", settle("kiwoom").getGoodsNm());
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalOrderKeepsExistingKiwoomName() {
|
||||
order.setSubsvcInnb(null);
|
||||
assertEquals("구독서비스상품 외5", settle("kiwoom").getGoodsNm());
|
||||
verifyNoInteractions(subscriptions);
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingSubscriptionNameStopsBeforeSavingPayment() {
|
||||
when(subscriptions.selectBOSubsvcDetail(any())).thenReturn(null);
|
||||
assertThrows(WnbtRestException.class, () -> settle("stripe"));
|
||||
verifyNoInteractions(mapper);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.wb.domainserver.domain.sopmal.login.controller;
|
||||
|
||||
import com.wb.domainserver.domain.admin.store.sopmal.service.BOMaSopmalService;
|
||||
import com.wb.domainserver.domain.gcone.login.service.LoginService;
|
||||
import com.wb.domainserver.realm.RealmContextHolder;
|
||||
import com.wb.domainserver.security.filter.JwtAuthenticationFilter;
|
||||
import com.wb.domainserver.security.user.SecurityUserDetailService;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class RefundPopupAuthenticationTest {
|
||||
private final String popupUrl = "http://localhost/sopmal/mpage/ordr/selectSetleCanclPopupForm"
|
||||
+ "?sopmalCd=SK&ordrInnb=test-order&ordrDlvyInnb=test-delivery";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RealmContextHolder.setRealm("sopmal");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
RealmContextHolder.clear();
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingRefreshTokenRedirectsToLoginOnlyOnce() throws Exception {
|
||||
LoginService service = mock(LoginService.class);
|
||||
LoginController controller = new LoginController(service, mock(BOMaSopmalService.class));
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/sopmal/tokens/refresh");
|
||||
request.setParameter("redirect_uri", popupUrl);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
// MockHttpServletResponse rejects a second redirect after commit, as Tomcat does.
|
||||
assertDoesNotThrow(() -> controller.refreshToken(request, response));
|
||||
assertEquals("/sopmal/login/page/sopmal?redirect_uri=" +
|
||||
java.net.URLEncoder.encode(popupUrl, StandardCharsets.UTF_8), response.getRedirectedUrl());
|
||||
verifyNoInteractions(service);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validRefreshTokenRenewsAndReturnsToPopupWithOrderParameters() throws Exception {
|
||||
LoginService service = mock(LoginService.class);
|
||||
LoginController controller = new LoginController(service, mock(BOMaSopmalService.class));
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/sopmal/tokens/refresh");
|
||||
request.setCookies(new Cookie("RFT", "mock-refresh-token"));
|
||||
request.setParameter("redirect_uri", popupUrl);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
controller.refreshToken(request, response);
|
||||
|
||||
verify(service).createRenewLoginToken(request, response);
|
||||
assertEquals(popupUrl, response.getRedirectedUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void expiredAccessCookieKeepsGetQueryWhenSendingToRefresh() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET",
|
||||
"/sopmal/mpage/ordr/selectSetleCanclPopupForm");
|
||||
request.setQueryString("sopmalCd=SK&ordrInnb=test-order&ordrDlvyInnb=test-delivery");
|
||||
request.setCookies(new Cookie("token_expire", "1"));
|
||||
FilterChain chain = mock(FilterChain.class);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
new JwtAuthenticationFilter(mock(SecurityUserDetailService.class)).doFilter(request, response, chain);
|
||||
|
||||
assertEquals("/sopmal/tokens/refresh?redirect_uri=" +
|
||||
java.net.URLEncoder.encode(popupUrl, StandardCharsets.UTF_8) + "&expire=p",
|
||||
response.getRedirectedUrl());
|
||||
verifyNoInteractions(chain);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validAccessTokenReachesPopupWithoutRedirect() throws Exception {
|
||||
SecurityUserDetailService service = mock(SecurityUserDetailService.class);
|
||||
when(service.loadUserByUsername("valid-test-token"))
|
||||
.thenReturn(User.withUsername("test").password("unused").authorities("ROLE_USER").build());
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET",
|
||||
"/sopmal/mpage/ordr/selectSetleCanclPopupForm");
|
||||
request.setCookies(new Cookie("ACT", "valid-test-token"));
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain chain = mock(FilterChain.class);
|
||||
|
||||
new JwtAuthenticationFilter(service).doFilter(request, response, chain);
|
||||
|
||||
verify(chain).doFilter(request, response);
|
||||
assertNull(response.getRedirectedUrl());
|
||||
assertTrue(SecurityContextHolder.getContext().getAuthentication().isAuthenticated());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidAccessJwtRedirectsToLogoutWithoutLoadingPopup() throws Exception {
|
||||
SecurityUserDetailService service = mock(SecurityUserDetailService.class);
|
||||
when(service.loadUserByUsername("invalid-test-token")).thenThrow(new JwtException("expired test token"));
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET",
|
||||
"/sopmal/mpage/ordr/selectSetleCanclPopupForm");
|
||||
request.setCookies(new Cookie("ACT", "invalid-test-token"));
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain chain = mock(FilterChain.class);
|
||||
|
||||
new JwtAuthenticationFilter(service).doFilter(request, response, chain);
|
||||
|
||||
assertEquals("/sopmal/logout", response.getRedirectedUrl());
|
||||
verifyNoInteractions(chain);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.wb.domainserver.domain.sopmal.stripe.controller;
|
||||
|
||||
import com.stripe.StripeClient;
|
||||
import com.stripe.model.checkout.Session;
|
||||
import com.wb.domainserver.domain.gcone.ordr.pay.service.BOPayService;
|
||||
import com.wb.domainserver.security.authentication.AnonymousUser;
|
||||
import com.wb.domainserver.security.authentication.MemberJwtContextHolder;
|
||||
import com.wb.domainserver.util.util.Util;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class StripeNavigationTest {
|
||||
private StripeController controller;
|
||||
private Session session;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
controller = new StripeController();
|
||||
BOPayService payService = mock(BOPayService.class);
|
||||
StripeClient client = mock(StripeClient.class, RETURNS_DEEP_STUBS);
|
||||
session = mock(Session.class);
|
||||
when(payService.createStripeClient("TEST")).thenReturn(client);
|
||||
when(client.v1().checkout().sessions().retrieve("cs_test_navigation")).thenReturn(session);
|
||||
when(session.getMetadata()).thenReturn(Map.of("mbrInnb", "42", "sopmalCd", "TEST"));
|
||||
ReflectionTestUtils.setField(controller, "payService", payService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void localReturnBaseOverridesShopDomainForBothCallbacks() {
|
||||
ReflectionTestUtils.setField(controller, "stripeReturnBaseUrl", "http://loc.soju1sake1.com:18500/");
|
||||
assertEquals("http://loc.soju1sake1.com:18500",
|
||||
controller.resolveStripeReturnBaseUrl("https://dev.example.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void otherProfilesKeepTheShopDomain() {
|
||||
assertEquals("https://shop.example.com",
|
||||
controller.resolveStripeReturnBaseUrl("https://shop.example.com/"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void redirectOnlyAcceptsTheOwningMember() throws Exception {
|
||||
AnonymousUser user = new AnonymousUser();
|
||||
user.setUserInnb(42);
|
||||
try (MockedStatic<MemberJwtContextHolder> context = mockStatic(MemberJwtContextHolder.class)) {
|
||||
context.when(MemberJwtContextHolder::getMemberInfo).thenReturn(user);
|
||||
when(session.getStatus()).thenReturn("open");
|
||||
when(session.getUrl()).thenReturn("https://checkout.stripe.com/test");
|
||||
ModelMap model = new ModelMap();
|
||||
assertEquals("sopmal/ordr/StripeRedirectR",
|
||||
controller.checkoutRedirect(model, "cs_test_navigation", "TEST"));
|
||||
assertEquals("https://checkout.stripe.com/test", model.get("checkoutUrl"));
|
||||
user.setUserInnb(99);
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> controller.checkoutRedirect(new ModelMap(), "cs_test_navigation", "TEST"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void expiredSessionDoesNotReopenCheckout() throws Exception {
|
||||
AnonymousUser user = new AnonymousUser();
|
||||
user.setUserInnb(42);
|
||||
try (MockedStatic<MemberJwtContextHolder> context = mockStatic(MemberJwtContextHolder.class)) {
|
||||
context.when(MemberJwtContextHolder::getMemberInfo).thenReturn(user);
|
||||
when(session.getStatus()).thenReturn("expired");
|
||||
ModelMap model = new ModelMap();
|
||||
controller.checkoutRedirect(model, "cs_test_navigation", "TEST");
|
||||
assertEquals("", model.get("checkoutUrl"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void unpaidReturnRendersOneNoticeForDesktopAndMobile() throws Exception {
|
||||
when(session.getStatus()).thenReturn("open");
|
||||
when(session.getPaymentStatus()).thenReturn("unpaid");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
try (MockedStatic<Util> util = mockStatic(Util.class)) {
|
||||
for (String device : new String[]{"sopmal", "sopmal_m"}) {
|
||||
util.when(() -> Util.getDeviceType(request)).thenReturn(device);
|
||||
ModelMap model = new ModelMap();
|
||||
assertEquals("sopmal/ordr/StripeCancelR",
|
||||
controller.stripeCancel(request, model, "cs_test_navigation", "TEST"));
|
||||
assertEquals("sopmal_m".equals(device), model.get("mobile"));
|
||||
assertFalse(model.containsKey("returnUrl"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void paidReturnDoesNotShowCancellation() throws Exception {
|
||||
when(session.getStatus()).thenReturn("complete");
|
||||
when(session.getPaymentStatus()).thenReturn("paid");
|
||||
ModelMap model = new ModelMap();
|
||||
assertEquals("sopmal/ordr/PayStripeSucsC", controller.stripeCancel(
|
||||
new MockHttpServletRequest(), model, "cs_test_navigation", "TEST"));
|
||||
assertEquals("/sopmal/ordr/selectOrdrCompt", model.get("returnUrl"));
|
||||
}
|
||||
}
|
||||
78
src/test/js/refund-popup.test.js
Normal file
78
src/test/js/refund-popup.test.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const source = fs.readFileSync(path.join(__dirname,
|
||||
'../../main/resources/static/js/sopmal/refund-popup.js'), 'utf8');
|
||||
const detail = 'http://local/sopmal/mpage/ordr/selectMbrSubsvcOrdrDtlInqire?ordrInnb=test-order&pageIndex=1';
|
||||
const endpoint = '/sopmal/mpage/ordr/selectSetleCanclPopupForm';
|
||||
const data = {sopmalCd: 'SK', ordrInnb: 'test-order', ordrDlvyInnb: 'delivery-1'};
|
||||
|
||||
async function run({status = 200, finalPath = endpoint, redirected = false, html = 'popup', networkError = false}) {
|
||||
const calls = {alerts: [], navigations: [], inserts: [], ready: 0, parse: 0, scriptRemoved: 0};
|
||||
const popup = {querySelectorAll: () => [{remove: () => calls.scriptRemoved++}]};
|
||||
const context = {
|
||||
URL, Object,
|
||||
window: {location: {href: detail, assign: url => calls.navigations.push(url)},
|
||||
alert: message => calls.alerts.push(message)},
|
||||
fetch: async (url, options) => {
|
||||
calls.request = {url: new URL(url), options};
|
||||
if (networkError) throw new Error('offline');
|
||||
return {status, ok: status >= 200 && status < 300, redirected,
|
||||
url: new URL(finalPath, detail).href, text: async () => html};
|
||||
},
|
||||
DOMParser: class {
|
||||
parseFromString(text) {
|
||||
calls.parse++;
|
||||
return {querySelector: selector => {
|
||||
assert.equal(selector, '#refund-pop');
|
||||
return text === 'popup' ? popup : null;
|
||||
}};
|
||||
}
|
||||
},
|
||||
$: selector => ({remove() {}, empty() {return this;},
|
||||
append: node => calls.inserts.push(node)})
|
||||
};
|
||||
vm.runInNewContext(source, context);
|
||||
await context.fn_load_refund_popup(endpoint, data, () => calls.ready++);
|
||||
return calls;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
for (const redirected of [false, true]) {
|
||||
const result = await run({redirected});
|
||||
assert.equal(result.ready, 1, 'normal / refreshed session opens the popup');
|
||||
assert.equal(result.alerts.length, 0);
|
||||
assert.equal(result.scriptRemoved, 1);
|
||||
assert.equal(result.request.options.method, 'GET');
|
||||
assert.equal(result.request.options.cache, 'no-store');
|
||||
assert.equal(result.request.url.searchParams.get('ordrInnb'), data.ordrInnb);
|
||||
assert.equal(result.request.url.searchParams.get('ordrDlvyInnb'), data.ordrDlvyInnb);
|
||||
}
|
||||
for (const auth of [
|
||||
{status: 401},
|
||||
{finalPath: '/sopmal/login/page/sopmal', redirected: true, html: '<script>login script</script>'},
|
||||
{finalPath: '/sopmal/main', redirected: true}
|
||||
]) {
|
||||
const result = await run(auth);
|
||||
assert.equal(result.ready, 0);
|
||||
assert.equal(result.inserts.length, 0, 'never insert a login/main page into the popup');
|
||||
assert.equal(result.parse, 0, 'do not parse or execute login HTML');
|
||||
assert.match(result.alerts[0], /로그인 시간이 만료/);
|
||||
const login = new URL(result.navigations[0]);
|
||||
assert.equal(login.pathname, '/sopmal/login');
|
||||
assert.equal(login.searchParams.get('redirect_uri'), detail);
|
||||
}
|
||||
for (const failure of [
|
||||
{status: 500}, {status: 403}, {html: ''}, {html: 'unexpected HTML'},
|
||||
{networkError: true}, {finalPath: '/', redirected: true}
|
||||
]) {
|
||||
const result = await run(failure);
|
||||
assert.equal(result.ready, 0);
|
||||
assert.equal(result.inserts.length, 0);
|
||||
assert.equal(result.navigations.length, 0, 'ordinary errors must not force a login');
|
||||
assert.match(result.alerts[0], /불러오지 못했습니다/);
|
||||
}
|
||||
console.log('PASS: 11 refund popup authentication, refresh, and error response scenarios');
|
||||
})().catch(error => {console.error(error); process.exitCode = 1;});
|
||||
40
src/test/js/stripe-navigation.test.js
Normal file
40
src/test/js/stripe-navigation.test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const view = fs.readFileSync(path.join(__dirname,
|
||||
'../../main/webapp/WEB-INF/views/sopmal/ordr/StripeRedirectR.jsp'), 'utf8');
|
||||
const script = view.match(/<script>([\s\S]*?)<\/script>/)[1];
|
||||
|
||||
function check({persisted = false, type = 'navigate', state = null, checkoutUrl = 'https://checkout.stripe.com/test'}, expected) {
|
||||
let handler;
|
||||
const calls = [];
|
||||
const context = {
|
||||
window: {
|
||||
addEventListener: (event, callback) => { assert.equal(event, 'pageshow'); handler = callback; },
|
||||
location: {
|
||||
href: 'http://local/sopmal/stripe/checkoutRedirect',
|
||||
replace: url => calls.push(['replace', url]),
|
||||
assign: url => calls.push(['assign', url])
|
||||
}
|
||||
},
|
||||
document: {getElementById: id => id === 'stripeCheckout'
|
||||
? {href: checkoutUrl, getAttribute: () => checkoutUrl}
|
||||
: {href: 'http://local/sopmal/stripe/stripeCancel'}},
|
||||
performance: {getEntriesByType: () => [{type}]},
|
||||
history: {state, replaceState: function (next) { this.state = next; }}
|
||||
};
|
||||
vm.runInNewContext(script, context);
|
||||
handler({persisted});
|
||||
assert.deepEqual(calls, [expected]);
|
||||
return {handler, calls};
|
||||
}
|
||||
|
||||
const initial = check({}, ['assign', 'https://checkout.stripe.com/test']);
|
||||
initial.handler({persisted: true});
|
||||
assert.deepEqual(initial.calls[1], ['replace', 'http://local/sopmal/stripe/stripeCancel']);
|
||||
check({type: 'back_forward'}, ['replace', 'http://local/sopmal/stripe/stripeCancel']);
|
||||
check({state: {stripeRedirected: true}}, ['replace', 'http://local/sopmal/stripe/stripeCancel']);
|
||||
check({checkoutUrl: ''}, ['replace', 'http://local/sopmal/stripe/stripeCancel']);
|
||||
console.log('PASS: initial entry, BFCache return, history return, repeat entry, expired session');
|
||||
Reference in New Issue
Block a user