|
|
@@ -0,0 +1,218 @@
|
|
|
+package com.ruoyi.app.pay;
|
|
|
+
|
|
|
+import com.fasterxml.jackson.databind.DeserializationFeature;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import com.ruoyi.app.pay.dto.PayMethodConfigSaveDto;
|
|
|
+import com.ruoyi.common.core.domain.AjaxResult;
|
|
|
+import com.ruoyi.common.exception.ServiceException;
|
|
|
+import com.ruoyi.common.utils.SecurityUtils;
|
|
|
+import com.ruoyi.common.utils.spring.SpringUtils;
|
|
|
+import com.ruoyi.system.domain.InfoUser;
|
|
|
+import com.ruoyi.system.domain.PaymentMethodConfig;
|
|
|
+import com.ruoyi.system.domain.PosStore;
|
|
|
+import com.ruoyi.system.mapper.InfoBankCardMapper;
|
|
|
+import com.ruoyi.system.mapper.InfoUserMapper;
|
|
|
+import com.ruoyi.system.mapper.PaymentMethodConfigMapper;
|
|
|
+import com.ruoyi.system.mapper.PosStoreLinePayMapper;
|
|
|
+import com.ruoyi.system.mapper.PosStoreMapper;
|
|
|
+import com.ruoyi.system.mapper.PosStoreOmgMapper;
|
|
|
+import com.ruoyi.system.service.MerchantAccessContext;
|
|
|
+import com.ruoyi.system.service.MerchantStoreAccessService;
|
|
|
+import com.ruoyi.system.service.PaymentMethodGateService;
|
|
|
+import com.ruoyi.system.service.StallPaymentAccessService;
|
|
|
+import com.ruoyi.system.utils.JwtUtil;
|
|
|
+import org.junit.jupiter.api.AfterAll;
|
|
|
+import org.junit.jupiter.api.BeforeAll;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.MockedStatic;
|
|
|
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
|
|
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
|
|
+import org.springframework.context.support.StaticMessageSource;
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
+
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Locale;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
+
|
|
|
+/** 031 商家备注:执行真实配置读写和商家接口,数据库访问以内存行替代,不调用外部服务。 */
|
|
|
+class PaymentMethodRemarkTest {
|
|
|
+ private final ObjectMapper json = new ObjectMapper()
|
|
|
+ .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
|
|
+ private final List<PaymentMethodConfig> stored = new ArrayList<>();
|
|
|
+ private PayMethodConfigController platform;
|
|
|
+ private MerchantPayMethodController merchant;
|
|
|
+ private PaymentMethodGateService gate;
|
|
|
+ private static ConfigurableListableBeanFactory originalBeanFactory;
|
|
|
+
|
|
|
+ @BeforeAll
|
|
|
+ static void messages() {
|
|
|
+ originalBeanFactory = (ConfigurableListableBeanFactory)
|
|
|
+ ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
|
|
|
+ DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
|
|
|
+ StaticMessageSource source = new StaticMessageSource();
|
|
|
+ source.addMessage("pay.method.config.invalid", Locale.getDefault(), "invalid config");
|
|
|
+ source.addMessage("no.action.success", Locale.getDefault(), "success");
|
|
|
+ factory.registerSingleton("messageSource", source);
|
|
|
+ new SpringUtils().postProcessBeanFactory(factory);
|
|
|
+ }
|
|
|
+
|
|
|
+ @AfterAll
|
|
|
+ static void restoreMessages() {
|
|
|
+ new SpringUtils().postProcessBeanFactory(originalBeanFactory);
|
|
|
+ }
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setup() {
|
|
|
+ PaymentMethodConfigMapper configs = mock(PaymentMethodConfigMapper.class);
|
|
|
+ // 故意返回其它维度,验证即使取回混合行,业务仍不得把骑手备注交给商家。
|
|
|
+ when(configs.selectList(any())).thenAnswer(invocation -> new ArrayList<>(stored));
|
|
|
+ when(configs.selectOne(any())).thenAnswer(invocation -> stored.isEmpty() ? null : stored.get(0));
|
|
|
+ when(configs.insert(any(PaymentMethodConfig.class))).thenAnswer(invocation -> {
|
|
|
+ PaymentMethodConfig row = invocation.getArgument(0);
|
|
|
+ row.setId((long) stored.size() + 1);
|
|
|
+ stored.add(row);
|
|
|
+ return 1;
|
|
|
+ });
|
|
|
+ when(configs.updateById(any(PaymentMethodConfig.class))).thenReturn(1);
|
|
|
+ platform = new PayMethodConfigController(configs);
|
|
|
+ InfoUserMapper users = mock(InfoUserMapper.class);
|
|
|
+ InfoUser owner = new InfoUser();
|
|
|
+ owner.setUserId(7L);
|
|
|
+ when(users.selectById(7L)).thenReturn(owner);
|
|
|
+ PosStoreMapper stores = mock(PosStoreMapper.class);
|
|
|
+ when(stores.selectList(any())).thenReturn(List.of());
|
|
|
+ InfoUser stallOwner = new InfoUser();
|
|
|
+ stallOwner.setUserType("4");
|
|
|
+ when(users.selectById(101L)).thenReturn(stallOwner);
|
|
|
+ PosStore stall = new PosStore();
|
|
|
+ stall.setId(11);
|
|
|
+ when(stores.selectById(11L)).thenReturn(stall);
|
|
|
+ InfoBankCardMapper cards = mock(InfoBankCardMapper.class);
|
|
|
+ PosStoreOmgMapper omg = mock(PosStoreOmgMapper.class);
|
|
|
+ PosStoreLinePayMapper line = mock(PosStoreLinePayMapper.class);
|
|
|
+ when(cards.selectCount(any())).thenReturn(0L);
|
|
|
+ when(omg.selectCount(any())).thenReturn(0L);
|
|
|
+ when(line.selectCount(any())).thenReturn(0L);
|
|
|
+ gate = new PaymentMethodGateService(configs, users, cards, omg, line, stores);
|
|
|
+ MerchantStoreAccessService access = mock(MerchantStoreAccessService.class);
|
|
|
+ MerchantAccessContext context = mock(MerchantAccessContext.class);
|
|
|
+ when(context.ownerUserId()).thenReturn(7L);
|
|
|
+ when(access.resolve(7L)).thenReturn(context);
|
|
|
+ StallPaymentAccessService stallAccess = mock(StallPaymentAccessService.class);
|
|
|
+ when(stallAccess.resolveStoreId(101L)).thenReturn(11L);
|
|
|
+ merchant = new MerchantPayMethodController(gate, users, access, stores, stallAccess);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void save(Map<String, Object> item) {
|
|
|
+ PayMethodConfigSaveDto dto = json.convertValue(Map.of("items", List.of(item)), PayMethodConfigSaveDto.class);
|
|
|
+ try (MockedStatic<SecurityUtils> security = mockStatic(SecurityUtils.class)) {
|
|
|
+ security.when(SecurityUtils::getUserId).thenReturn(1L);
|
|
|
+ platform.save(dto);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> item(String remark) {
|
|
|
+ return Map.of("methodCode", "LINE_PAY", "scope", "MERCHANT", "enabled", 1, "remark", remark);
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<?, ?> merchantData(long userId) {
|
|
|
+ return (Map<?, ?>) merchant.get(JwtUtil.setToken(String.valueOf(userId), "test")).get(AjaxResult.DATA_TAG);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void savedRemarkReturnsInPlatformMatrixAndMerchantSettings() {
|
|
|
+ save(item("手續費 2.31%;\n消費後第三天入帳"));
|
|
|
+ assertEquals("手續費 2.31%;\n消費後第三天入帳",
|
|
|
+ json.valueToTree(platform.list().get(AjaxResult.DATA_TAG)).get(2).path("remark").asText(null));
|
|
|
+ assertEquals(Map.of("LINE_PAY", "手續費 2.31%;\n消費後第三天入帳"), merchantData(7L).get("remarks"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void existingRemarkCanBeUpdatedAndCleared() {
|
|
|
+ save(item("原說明"));
|
|
|
+ save(item("新說明"));
|
|
|
+ assertEquals(Map.of("LINE_PAY", "新說明"), merchantData(7L).get("remarks"));
|
|
|
+ save(item(" "));
|
|
|
+ assertEquals("", json.valueToTree(stored.get(0)).path("remark").asText(null));
|
|
|
+ assertEquals(Map.of(), merchantData(7L).get("remarks"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void oldClientSavingOnlySwitchPreservesRemark() {
|
|
|
+ save(item("三天入帳"));
|
|
|
+ save(Map.of("methodCode", "LINE_PAY", "scope", "MERCHANT", "enabled", 0));
|
|
|
+ assertEquals("三天入帳", json.valueToTree(stored.get(0)).path("remark").asText(null));
|
|
|
+ assertEquals(0, stored.get(0).getEnabled());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void noConfiguredRemarksReturnsEmptyMapForMerchantAndStall() {
|
|
|
+ assertEquals(Map.of(), merchantData(7L).get("remarks"));
|
|
|
+ assertEquals(Map.of(), merchantData(101L).get("remarks"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void stallSeesSamePlatformRemarkAsOrdinaryMerchant() {
|
|
|
+ save(item("按合約費率"));
|
|
|
+ assertEquals(Map.of("LINE_PAY", "按合約費率"), merchantData(101L).get("remarks"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void rejectsOverlongRemarkBeforeAnyWrite() {
|
|
|
+ assertThrows(ServiceException.class, () -> save(item("字".repeat(501))));
|
|
|
+ assertTrue(stored.isEmpty());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void invalidLaterItemPreventsWritingWholeBatch() {
|
|
|
+ PayMethodConfigSaveDto dto = json.convertValue(Map.of("items", List.of(item("可保存的說明"),
|
|
|
+ item("字".repeat(501)))), PayMethodConfigSaveDto.class);
|
|
|
+ assertThrows(ServiceException.class, () -> platform.save(dto));
|
|
|
+ assertTrue(stored.isEmpty());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void merchantCannotOverwritePlatformRemarkThroughSelectionSave() {
|
|
|
+ save(item("平台維護"));
|
|
|
+ com.ruoyi.app.pay.dto.MerchantPayMethodSaveDto dto = json.convertValue(
|
|
|
+ Map.of("methodCodes", List.of("LINE_PAY"), "remarks", Map.of("LINE_PAY", "商家改寫")),
|
|
|
+ com.ruoyi.app.pay.dto.MerchantPayMethodSaveDto.class);
|
|
|
+ merchant.save(JwtUtil.setToken("7", "test"), dto);
|
|
|
+ assertEquals(Map.of("LINE_PAY", "平台維護"), merchantData(7L).get("remarks"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void acceptsMaximumLengthRemark() {
|
|
|
+ save(item("字".repeat(500)));
|
|
|
+ assertEquals(Map.of("LINE_PAY", "字".repeat(500)), merchantData(7L).get("remarks"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void rejectsRemarkOnRiderScope() {
|
|
|
+ assertThrows(ServiceException.class, () -> save(Map.of("methodCode", "LINE_PAY",
|
|
|
+ "scope", "RIDER_FLASH", "enabled", 1, "remark", "不應寫入騎手")));
|
|
|
+ assertTrue(stored.isEmpty());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void riderRowsDoNotLeakIntoMerchantRemarks() {
|
|
|
+ stored.add(json.convertValue(Map.of("methodCode", "LINE_PAY", "scope", "RIDER_FLASH",
|
|
|
+ "enabled", 1, "remark", "騎手資料"), PaymentMethodConfig.class));
|
|
|
+ assertEquals(Map.of(), merchantData(7L).get("remarks"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void remarkDoesNotChangeAvailabilityOrCheckoutResponse() {
|
|
|
+ save(item("僅說明,不是費率設定"));
|
|
|
+ assertFalse(gate.listAvailableForMerchantSettings(7L, List.of()).stream()
|
|
|
+ .filter(option -> "LINE_PAY".equals(option.getMethodCode())).findFirst().orElseThrow().isReady());
|
|
|
+ assertFalse(json.valueToTree(gate.listAvailable(PaymentMethodGateService.GateScope.RIDER_FLASH,
|
|
|
+ null, null)).toString().contains("remark"));
|
|
|
+ }
|
|
|
+}
|