fix(withdraw): tenant filter + enums + tests
This commit is contained in:
@@ -19,12 +19,23 @@ import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
|
|||||||
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
|
||||||
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
|
||||||
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
|
||||||
|
import com.starry.admin.modules.order.module.constant.OrderConstant;
|
||||||
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
|
||||||
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
|
||||||
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
|
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
|
||||||
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
|
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
|
||||||
|
import com.starry.admin.modules.shop.module.constant.CouponUseState;
|
||||||
import com.starry.admin.modules.shop.module.entity.PlayCommodityAndLevelInfoEntity;
|
import com.starry.admin.modules.shop.module.entity.PlayCommodityAndLevelInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.entity.PlayCouponInfoEntity;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponDiscountType;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponObtainChannel;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponOnlineState;
|
||||||
|
import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType;
|
||||||
import com.starry.admin.modules.shop.service.IPlayCommodityAndLevelInfoService;
|
import com.starry.admin.modules.shop.service.IPlayCommodityAndLevelInfoService;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
|
||||||
|
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
|
||||||
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
|
import com.starry.admin.modules.system.module.entity.SysTenantEntity;
|
||||||
import com.starry.admin.modules.system.module.entity.SysUserEntity;
|
import com.starry.admin.modules.system.module.entity.SysUserEntity;
|
||||||
import com.starry.admin.modules.system.service.ISysTenantService;
|
import com.starry.admin.modules.system.service.ISysTenantService;
|
||||||
@@ -117,6 +128,11 @@ public class WxOauthController {
|
|||||||
@Resource
|
@Resource
|
||||||
private IEarningsService earningsService;
|
private IEarningsService earningsService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IPlayCouponInfoService couponInfoService;
|
||||||
|
@Resource
|
||||||
|
private IPlayCouponDetailsService couponDetailsService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private Environment environment;
|
private Environment environment;
|
||||||
|
|
||||||
@@ -125,6 +141,30 @@ public class WxOauthController {
|
|||||||
|
|
||||||
private static final String TEST_AUTH_HEADER = "X-Test-Auth";
|
private static final String TEST_AUTH_HEADER = "X-Test-Auth";
|
||||||
|
|
||||||
|
private enum SwitchState {
|
||||||
|
DISABLED("0"),
|
||||||
|
ENABLED("1");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
SwitchState(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SwitchState fromCode(String code) {
|
||||||
|
for (SwitchState value : values()) {
|
||||||
|
if (value.code.equals(code)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("invalid switch state");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isTestAuthEnabled() {
|
private boolean isTestAuthEnabled() {
|
||||||
String[] profiles = environment == null ? new String[0] : environment.getActiveProfiles();
|
String[] profiles = environment == null ? new String[0] : environment.getActiveProfiles();
|
||||||
Set<String> active = Stream.of(profiles == null ? new String[0] : profiles)
|
Set<String> active = Stream.of(profiles == null ? new String[0] : profiles)
|
||||||
@@ -387,6 +427,111 @@ public class WxOauthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "测试用:种子数据(优惠券)", notes = "创建并直接发放给顾客 1 张满减券,用于 coupon E2E")
|
||||||
|
@PostMapping("/e2e/seed/coupon")
|
||||||
|
public R seedCoupon(
|
||||||
|
@RequestHeader(value = TEST_AUTH_HEADER, required = false) String testAuth,
|
||||||
|
@RequestParam(value = "customerId", required = false, defaultValue = ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
|
||||||
|
String customerId,
|
||||||
|
@RequestParam(value = "placeType", required = false, defaultValue = "0") String placeType,
|
||||||
|
@RequestParam("discountAmount") String discountAmount,
|
||||||
|
@RequestParam(value = "useMinAmount", required = false, defaultValue = "0") String useMinAmount) {
|
||||||
|
if (!isTestAuthHeaderValid(testAuth)) {
|
||||||
|
return rejectTestAuth();
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(customerId) || !StringUtils.hasText(discountAmount) || !StringUtils.hasText(useMinAmount)) {
|
||||||
|
return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "missing required fields");
|
||||||
|
}
|
||||||
|
final String tenantId = ApiTestDataSeeder.DEFAULT_TENANT_ID;
|
||||||
|
try (TenantScope ignored = TenantScope.use(tenantId)) {
|
||||||
|
PlayCustomUserInfoEntity customer = customUserInfoService.getById(customerId);
|
||||||
|
if (customer == null) {
|
||||||
|
return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "customer not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
OrderConstant.PlaceType parsedPlaceType;
|
||||||
|
try {
|
||||||
|
parsedPlaceType = OrderConstant.PlaceType.fromCode(placeType);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "invalid placeType");
|
||||||
|
}
|
||||||
|
|
||||||
|
java.math.BigDecimal discount;
|
||||||
|
java.math.BigDecimal minAmount;
|
||||||
|
try {
|
||||||
|
discount = new java.math.BigDecimal(discountAmount);
|
||||||
|
minAmount = new java.math.BigDecimal(useMinAmount);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "invalid amount");
|
||||||
|
}
|
||||||
|
if (discount.signum() <= 0) {
|
||||||
|
return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "discountAmount must be > 0");
|
||||||
|
}
|
||||||
|
if (minAmount.signum() < 0) {
|
||||||
|
return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "useMinAmount must be >= 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
String couponId = "e2e-cpn-" + IdUtils.getUuid();
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
java.util.Date nowDate = java.util.Date.from(now.atZone(java.time.ZoneId.systemDefault()).toInstant());
|
||||||
|
|
||||||
|
PlayCouponInfoEntity coupon = new PlayCouponInfoEntity();
|
||||||
|
coupon.setId(couponId);
|
||||||
|
coupon.setTenantId(tenantId);
|
||||||
|
coupon.setCouponName("E2E满减券-" + couponId);
|
||||||
|
coupon.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode());
|
||||||
|
coupon.setUseMinAmount(minAmount);
|
||||||
|
coupon.setDiscountType(CouponDiscountType.FULL_REDUCTION.getCode());
|
||||||
|
coupon.setDiscountContent("E2E满减");
|
||||||
|
coupon.setDiscountAmount(discount);
|
||||||
|
coupon.setAttributionDiscounts("0");
|
||||||
|
coupon.setPlaceType(List.of(parsedPlaceType.getCode()));
|
||||||
|
coupon.setClerkType("0");
|
||||||
|
coupon.setCouponQuantity(1);
|
||||||
|
coupon.setIssuedQuantity(0);
|
||||||
|
coupon.setRemainingQuantity(1);
|
||||||
|
coupon.setClerkObtainedMaxQuantity(1);
|
||||||
|
coupon.setClaimConditionType(CouponClaimConditionType.ALL.code());
|
||||||
|
coupon.setCustomWhitelist(new ArrayList<>());
|
||||||
|
coupon.setCustomLevelCheckType("0");
|
||||||
|
coupon.setCustomSexCheckType("0");
|
||||||
|
coupon.setNewUser("0");
|
||||||
|
coupon.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
|
||||||
|
coupon.setProductiveTime(now.minusDays(1));
|
||||||
|
coupon.setExpirationTime(now.plusDays(30));
|
||||||
|
coupon.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
coupon.setCreatedTime(nowDate);
|
||||||
|
coupon.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
coupon.setUpdatedTime(nowDate);
|
||||||
|
couponInfoService.save(coupon);
|
||||||
|
|
||||||
|
String detailId = "e2e-cpn-detail-" + IdUtils.getUuid();
|
||||||
|
PlayCouponDetailsEntity detail = new PlayCouponDetailsEntity();
|
||||||
|
detail.setId(detailId);
|
||||||
|
detail.setTenantId(tenantId);
|
||||||
|
detail.setCustomId(customer.getId());
|
||||||
|
detail.setCouponId(couponId);
|
||||||
|
detail.setCustomNickname(customer.getNickname());
|
||||||
|
detail.setCustomLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
|
||||||
|
detail.setObtainingChannels(CouponObtainChannel.SELF_SERVICE.getCode());
|
||||||
|
detail.setUseState(CouponUseState.UNUSED.getCode());
|
||||||
|
detail.setObtainingTime(now);
|
||||||
|
detail.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
detail.setCreatedTime(nowDate);
|
||||||
|
detail.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
|
||||||
|
detail.setUpdatedTime(nowDate);
|
||||||
|
couponDetailsService.save(detail);
|
||||||
|
|
||||||
|
return R.ok(new E2eCouponSeedResponse(
|
||||||
|
customer.getId(),
|
||||||
|
couponId,
|
||||||
|
detailId,
|
||||||
|
discount.toPlainString(),
|
||||||
|
minAmount.toPlainString(),
|
||||||
|
parsedPlaceType.getCode()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ApiOperation(value = "测试用:种子数据(店员上下架)", notes = "修改店员 listingState,用于下架/不可用分支 E2E")
|
@ApiOperation(value = "测试用:种子数据(店员上下架)", notes = "修改店员 listingState,用于下架/不可用分支 E2E")
|
||||||
@PostMapping("/e2e/seed/clerk-listing-state")
|
@PostMapping("/e2e/seed/clerk-listing-state")
|
||||||
public R seedClerkListingState(
|
public R seedClerkListingState(
|
||||||
@@ -412,6 +557,47 @@ public class WxOauthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "测试用:种子数据(店员可接单状态)", notes = "更新 onlineState/listingState/randomOrderState/displayState,用于随机单成功/失败分支 E2E")
|
||||||
|
@PostMapping("/e2e/seed/clerk-availability")
|
||||||
|
public R seedClerkAvailability(
|
||||||
|
@RequestHeader(value = TEST_AUTH_HEADER, required = false) String testAuth,
|
||||||
|
@RequestParam("clerkId") String clerkId,
|
||||||
|
@RequestParam(value = "onlineState", required = false, defaultValue = "1") String onlineState,
|
||||||
|
@RequestParam(value = "listingState", required = false, defaultValue = "1") String listingState,
|
||||||
|
@RequestParam(value = "randomOrderState", required = false, defaultValue = "1") String randomOrderState,
|
||||||
|
@RequestParam(value = "displayState", required = false, defaultValue = "1") String displayState) {
|
||||||
|
if (!isTestAuthHeaderValid(testAuth)) {
|
||||||
|
return rejectTestAuth();
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(clerkId)) {
|
||||||
|
return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "missing required fields");
|
||||||
|
}
|
||||||
|
final String tenantId = ApiTestDataSeeder.DEFAULT_TENANT_ID;
|
||||||
|
try (TenantScope ignored = TenantScope.use(tenantId)) {
|
||||||
|
PlayClerkUserInfoEntity clerk = clerkUserInfoService.getById(clerkId);
|
||||||
|
if (clerk == null) {
|
||||||
|
return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "clerk not found");
|
||||||
|
}
|
||||||
|
SwitchState online = SwitchState.fromCode(onlineState);
|
||||||
|
ListingStatus listing = ListingStatus.fromCode(listingState);
|
||||||
|
SwitchState random = SwitchState.fromCode(randomOrderState);
|
||||||
|
SwitchState display = SwitchState.fromCode(displayState);
|
||||||
|
clerk.setOnlineState(online.getCode());
|
||||||
|
clerk.setListingState(listing.getCode());
|
||||||
|
clerk.setRandomOrderState(random.getCode());
|
||||||
|
clerk.setDisplayState(display.getCode());
|
||||||
|
clerkUserInfoService.updateById(clerk);
|
||||||
|
return R.ok(new E2eClerkAvailabilitySeedResponse(
|
||||||
|
clerkId,
|
||||||
|
online.getCode(),
|
||||||
|
listing.getCode(),
|
||||||
|
random.getCode(),
|
||||||
|
display.getCode()));
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ApiOperation(value = "测试用:种子数据(冻结收益)", notes = "插入 frozen earning line(有 order_id, amount>0),用于冻结/不可提现分支 E2E")
|
@ApiOperation(value = "测试用:种子数据(冻结收益)", notes = "插入 frozen earning line(有 order_id, amount>0),用于冻结/不可提现分支 E2E")
|
||||||
@PostMapping("/e2e/seed/frozen-earnings")
|
@PostMapping("/e2e/seed/frozen-earnings")
|
||||||
public R seedFrozenEarnings(
|
public R seedFrozenEarnings(
|
||||||
@@ -446,6 +632,50 @@ public class WxOauthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOperation(value = "测试用:种子数据(PC端受限账号)", notes = "创建一个非超级管理员账号(无菜单权限),用于权限可见性 E2E")
|
||||||
|
@PostMapping("/e2e/seed/pc-tenant-user/no-menu")
|
||||||
|
public R seedPcTenantNoMenuUser(
|
||||||
|
@RequestHeader(value = TEST_AUTH_HEADER, required = false) String testAuth,
|
||||||
|
@RequestParam(value = "userName", required = false) String userName) {
|
||||||
|
if (!isTestAuthHeaderValid(testAuth)) {
|
||||||
|
return rejectTestAuth();
|
||||||
|
}
|
||||||
|
final String tenantId = ApiTestDataSeeder.DEFAULT_TENANT_ID;
|
||||||
|
final String tenantKey = ApiTestDataSeeder.DEFAULT_TENANT_KEY;
|
||||||
|
final String rawPassword = "apitest-secret";
|
||||||
|
String resolvedUserName = StringUtils.hasText(userName)
|
||||||
|
? userName
|
||||||
|
// LoginService enforces username length <= 20.
|
||||||
|
: ("nm" + IdUtils.getUuid().substring(0, 8));
|
||||||
|
|
||||||
|
SysUserEntity existing = sysUserService.selectUserByUserNameAndTenantId(resolvedUserName, tenantId);
|
||||||
|
if (existing == null) {
|
||||||
|
SysUserEntity user = new SysUserEntity();
|
||||||
|
user.setUserId("user-" + resolvedUserName);
|
||||||
|
user.setUserCode(resolvedUserName);
|
||||||
|
user.setPassWord(passwordEncoder.encode(rawPassword));
|
||||||
|
user.setRealName("E2E NoMenu");
|
||||||
|
user.setUserNickname("E2E-NoMenu");
|
||||||
|
user.setStatus(0);
|
||||||
|
user.setUserType(1);
|
||||||
|
user.setTenantId(tenantId);
|
||||||
|
user.setMobile("13800000009");
|
||||||
|
user.setAddTime(LocalDateTime.now());
|
||||||
|
user.setSuperAdmin(Boolean.FALSE);
|
||||||
|
sysUserService.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
SysUserEntity created = sysUserService.selectUserByUserNameAndTenantId(resolvedUserName, tenantId);
|
||||||
|
if (created == null) {
|
||||||
|
return R.error(ResultCodeEnum.VALIDATE_FAILED.getCode(), "failed to create user");
|
||||||
|
}
|
||||||
|
return R.ok(new E2ePcTenantNoMenuUserSeedResponse(
|
||||||
|
tenantKey,
|
||||||
|
created.getUserCode(),
|
||||||
|
rawPassword,
|
||||||
|
created.getUserId()));
|
||||||
|
}
|
||||||
|
|
||||||
@ApiOperation(value = "测试用:种子数据(tenantB)", notes = "创建 tenant-key-apitest-b 及其 admin/clerk,用于多租户隔离 E2E")
|
@ApiOperation(value = "测试用:种子数据(tenantB)", notes = "创建 tenant-key-apitest-b 及其 admin/clerk,用于多租户隔离 E2E")
|
||||||
@PostMapping("/e2e/seed/tenant-b")
|
@PostMapping("/e2e/seed/tenant-b")
|
||||||
public R seedTenantB(
|
public R seedTenantB(
|
||||||
@@ -745,6 +975,54 @@ public class WxOauthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class E2eCouponSeedResponse {
|
||||||
|
private final String customerId;
|
||||||
|
private final String couponId;
|
||||||
|
private final String couponDetailId;
|
||||||
|
private final String discountAmount;
|
||||||
|
private final String useMinAmount;
|
||||||
|
private final String placeType;
|
||||||
|
|
||||||
|
private E2eCouponSeedResponse(
|
||||||
|
String customerId,
|
||||||
|
String couponId,
|
||||||
|
String couponDetailId,
|
||||||
|
String discountAmount,
|
||||||
|
String useMinAmount,
|
||||||
|
String placeType) {
|
||||||
|
this.customerId = customerId;
|
||||||
|
this.couponId = couponId;
|
||||||
|
this.couponDetailId = couponDetailId;
|
||||||
|
this.discountAmount = discountAmount;
|
||||||
|
this.useMinAmount = useMinAmount;
|
||||||
|
this.placeType = placeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCustomerId() {
|
||||||
|
return customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCouponId() {
|
||||||
|
return couponId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCouponDetailId() {
|
||||||
|
return couponDetailId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDiscountAmount() {
|
||||||
|
return discountAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUseMinAmount() {
|
||||||
|
return useMinAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlaceType() {
|
||||||
|
return placeType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class E2eClerkListingSeedResponse {
|
private static final class E2eClerkListingSeedResponse {
|
||||||
private final String clerkId;
|
private final String clerkId;
|
||||||
private final String listingState;
|
private final String listingState;
|
||||||
@@ -763,6 +1041,47 @@ public class WxOauthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class E2eClerkAvailabilitySeedResponse {
|
||||||
|
private final String clerkId;
|
||||||
|
private final String onlineState;
|
||||||
|
private final String listingState;
|
||||||
|
private final String randomOrderState;
|
||||||
|
private final String displayState;
|
||||||
|
|
||||||
|
private E2eClerkAvailabilitySeedResponse(
|
||||||
|
String clerkId,
|
||||||
|
String onlineState,
|
||||||
|
String listingState,
|
||||||
|
String randomOrderState,
|
||||||
|
String displayState) {
|
||||||
|
this.clerkId = clerkId;
|
||||||
|
this.onlineState = onlineState;
|
||||||
|
this.listingState = listingState;
|
||||||
|
this.randomOrderState = randomOrderState;
|
||||||
|
this.displayState = displayState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClerkId() {
|
||||||
|
return clerkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOnlineState() {
|
||||||
|
return onlineState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getListingState() {
|
||||||
|
return listingState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRandomOrderState() {
|
||||||
|
return randomOrderState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayState() {
|
||||||
|
return displayState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class E2eFrozenEarningsSeedResponse {
|
private static final class E2eFrozenEarningsSeedResponse {
|
||||||
private final String clerkId;
|
private final String clerkId;
|
||||||
private final String orderId;
|
private final String orderId;
|
||||||
@@ -787,6 +1106,36 @@ public class WxOauthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class E2ePcTenantNoMenuUserSeedResponse {
|
||||||
|
private final String tenantKey;
|
||||||
|
private final String userName;
|
||||||
|
private final String password;
|
||||||
|
private final String userId;
|
||||||
|
|
||||||
|
private E2ePcTenantNoMenuUserSeedResponse(String tenantKey, String userName, String password, String userId) {
|
||||||
|
this.tenantKey = tenantKey;
|
||||||
|
this.userName = userName;
|
||||||
|
this.password = password;
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTenantKey() {
|
||||||
|
return tenantKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserName() {
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static final class E2eTenantSeedResponse {
|
private static final class E2eTenantSeedResponse {
|
||||||
private final String tenantId;
|
private final String tenantId;
|
||||||
private final String tenantKey;
|
private final String tenantKey;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
|||||||
import com.starry.admin.modules.withdraw.entity.WithdrawalLogEntity;
|
import com.starry.admin.modules.withdraw.entity.WithdrawalLogEntity;
|
||||||
import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
|
import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
|
||||||
import com.starry.admin.modules.withdraw.enums.EarningsSourceType;
|
import com.starry.admin.modules.withdraw.enums.EarningsSourceType;
|
||||||
|
import com.starry.admin.modules.withdraw.enums.WithdrawalRequestStatus;
|
||||||
import com.starry.admin.modules.withdraw.service.IEarningsAdjustmentService;
|
import com.starry.admin.modules.withdraw.service.IEarningsAdjustmentService;
|
||||||
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
import com.starry.admin.modules.withdraw.service.IWithdrawalLogService;
|
import com.starry.admin.modules.withdraw.service.IWithdrawalLogService;
|
||||||
@@ -61,11 +62,43 @@ public class WxWithdrawController {
|
|||||||
@GetMapping("/balance")
|
@GetMapping("/balance")
|
||||||
public TypedR<ClerkWithdrawBalanceVo> getBalance() {
|
public TypedR<ClerkWithdrawBalanceVo> getBalance() {
|
||||||
String clerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId();
|
String clerkId = ThreadLocalRequestDetail.getClerkUserInfo().getId();
|
||||||
|
String tenantId = SecurityUtils.getTenantId();
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
BigDecimal available = earningsService.getAvailableAmount(clerkId, now);
|
BigDecimal available = earningsService.getAvailableAmount(clerkId, now);
|
||||||
BigDecimal pending = earningsService.getPendingAmount(clerkId, now);
|
BigDecimal pending = earningsService.getPendingAmount(clerkId, now);
|
||||||
LocalDateTime nextUnlock = earningsService.getNextUnlockTime(clerkId, now);
|
LocalDateTime nextUnlock = earningsService.getNextUnlockTime(clerkId, now);
|
||||||
return TypedR.ok(new ClerkWithdrawBalanceVo(available, pending, nextUnlock));
|
WithdrawalRequestEntity active = withdrawalService.lambdaQuery()
|
||||||
|
.eq(WithdrawalRequestEntity::getTenantId, tenantId)
|
||||||
|
.eq(WithdrawalRequestEntity::getClerkId, clerkId)
|
||||||
|
.in(WithdrawalRequestEntity::getStatus,
|
||||||
|
WithdrawalRequestStatus.PENDING.getCode(),
|
||||||
|
WithdrawalRequestStatus.PROCESSING.getCode())
|
||||||
|
.orderByDesc(WithdrawalRequestEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
|
||||||
|
Boolean locked = active != null;
|
||||||
|
String lockReason = locked ? "当前已有一笔提现申请在途,请等待处理完成后再申请。" : "";
|
||||||
|
ClerkWithdrawBalanceVo.ActiveRequest activeRequest = null;
|
||||||
|
if (active != null) {
|
||||||
|
Date createdTime = active.getCreatedTime();
|
||||||
|
LocalDateTime createdAt = createdTime == null ? null
|
||||||
|
: LocalDateTime.ofInstant(createdTime.toInstant(), ZoneId.systemDefault());
|
||||||
|
activeRequest = ClerkWithdrawBalanceVo.ActiveRequest.builder()
|
||||||
|
.amount(active.getAmount())
|
||||||
|
.status(active.getStatus())
|
||||||
|
.createdTime(createdAt == null ? "" : createdAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
ClerkWithdrawBalanceVo vo = new ClerkWithdrawBalanceVo();
|
||||||
|
vo.setAvailable(available);
|
||||||
|
vo.setPending(pending);
|
||||||
|
vo.setNextUnlockAt(nextUnlock);
|
||||||
|
vo.setWithdrawLocked(locked);
|
||||||
|
vo.setWithdrawLockReason(lockReason);
|
||||||
|
vo.setActiveRequest(activeRequest);
|
||||||
|
return TypedR.ok(vo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ClerkUserLogin
|
@ClerkUserLogin
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.enums;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public enum EarningsLineStatus {
|
||||||
|
FROZEN("frozen"),
|
||||||
|
AVAILABLE("available"),
|
||||||
|
WITHDRAWING("withdrawing"),
|
||||||
|
WITHDRAWN("withdrawn"),
|
||||||
|
REVERSED("reversed");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
EarningsLineStatus(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EarningsLineStatus fromCode(String code) {
|
||||||
|
return Arrays.stream(values())
|
||||||
|
.filter(it -> it.code.equals(code))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("invalid earnings line status: " + code));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.starry.admin.modules.withdraw.enums;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public enum WithdrawalRequestStatus {
|
||||||
|
PENDING("pending"),
|
||||||
|
PROCESSING("processing"),
|
||||||
|
SUCCESS("success"),
|
||||||
|
FAILED("failed"),
|
||||||
|
CANCELED("canceled"),
|
||||||
|
REJECTED("rejected");
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
WithdrawalRequestStatus(String code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WithdrawalRequestStatus fromCode(String code) {
|
||||||
|
return Arrays.stream(values())
|
||||||
|
.filter(it -> it.code.equals(code))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("invalid withdrawal request status: " + code));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import com.starry.admin.common.exception.CustomException;
|
|||||||
import com.starry.admin.modules.withdraw.entity.ClerkPayeeProfileEntity;
|
import com.starry.admin.modules.withdraw.entity.ClerkPayeeProfileEntity;
|
||||||
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
|
||||||
import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
|
import com.starry.admin.modules.withdraw.entity.WithdrawalRequestEntity;
|
||||||
|
import com.starry.admin.modules.withdraw.enums.EarningsLineStatus;
|
||||||
|
import com.starry.admin.modules.withdraw.enums.WithdrawalRequestStatus;
|
||||||
import com.starry.admin.modules.withdraw.mapper.WithdrawalRequestMapper;
|
import com.starry.admin.modules.withdraw.mapper.WithdrawalRequestMapper;
|
||||||
import com.starry.admin.modules.withdraw.service.IClerkPayeeProfileService;
|
import com.starry.admin.modules.withdraw.service.IClerkPayeeProfileService;
|
||||||
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
import com.starry.admin.modules.withdraw.service.IEarningsService;
|
||||||
@@ -43,6 +45,20 @@ public class WithdrawalServiceImpl extends ServiceImpl<WithdrawalRequestMapper,
|
|||||||
}
|
}
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
String tenantId = SecurityUtils.getTenantId();
|
String tenantId = SecurityUtils.getTenantId();
|
||||||
|
|
||||||
|
WithdrawalRequestEntity active = this.lambdaQuery()
|
||||||
|
.eq(WithdrawalRequestEntity::getTenantId, tenantId)
|
||||||
|
.eq(WithdrawalRequestEntity::getClerkId, clerkId)
|
||||||
|
.in(WithdrawalRequestEntity::getStatus,
|
||||||
|
WithdrawalRequestStatus.PENDING.getCode(),
|
||||||
|
WithdrawalRequestStatus.PROCESSING.getCode())
|
||||||
|
.orderByDesc(WithdrawalRequestEntity::getCreatedTime)
|
||||||
|
.last("limit 1")
|
||||||
|
.one();
|
||||||
|
if (active != null) {
|
||||||
|
throw new CustomException("仅可同时存在一笔提现申请,请等待当前申请处理完成后再提交");
|
||||||
|
}
|
||||||
|
|
||||||
ClerkPayeeProfileEntity payeeProfile = clerkPayeeProfileService.getByClerk(tenantId, clerkId);
|
ClerkPayeeProfileEntity payeeProfile = clerkPayeeProfileService.getByClerk(tenantId, clerkId);
|
||||||
if (payeeProfile == null || !StringUtils.hasText(payeeProfile.getQrCodeUrl())) {
|
if (payeeProfile == null || !StringUtils.hasText(payeeProfile.getQrCodeUrl())) {
|
||||||
throw new CustomException("请先上传支付宝收款码");
|
throw new CustomException("请先上传支付宝收款码");
|
||||||
@@ -66,7 +82,7 @@ public class WithdrawalServiceImpl extends ServiceImpl<WithdrawalRequestMapper,
|
|||||||
boolean updated = earningsService.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
|
boolean updated = earningsService.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
|
||||||
.eq(EarningsLineEntity::getId, line.getId())
|
.eq(EarningsLineEntity::getId, line.getId())
|
||||||
.eq(EarningsLineEntity::getStatus, line.getStatus()) // Verify status unchanged
|
.eq(EarningsLineEntity::getStatus, line.getStatus()) // Verify status unchanged
|
||||||
.set(EarningsLineEntity::getStatus, "withdrawing")
|
.set(EarningsLineEntity::getStatus, EarningsLineStatus.WITHDRAWING.getCode())
|
||||||
.set(EarningsLineEntity::getWithdrawalId, tempWithdrawalId));
|
.set(EarningsLineEntity::getWithdrawalId, tempWithdrawalId));
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
// Another request already took this line
|
// Another request already took this line
|
||||||
@@ -88,7 +104,7 @@ public class WithdrawalServiceImpl extends ServiceImpl<WithdrawalRequestMapper,
|
|||||||
req.setNetAmount(amount);
|
req.setNetAmount(amount);
|
||||||
req.setDestAccount(payeeProfile.getDisplayName());
|
req.setDestAccount(payeeProfile.getDisplayName());
|
||||||
req.setPayeeSnapshot(snapshotJson);
|
req.setPayeeSnapshot(snapshotJson);
|
||||||
req.setStatus("pending");
|
req.setStatus(WithdrawalRequestStatus.PENDING.getCode());
|
||||||
req.setOutBizNo(req.getId());
|
req.setOutBizNo(req.getId());
|
||||||
this.save(req);
|
this.save(req);
|
||||||
|
|
||||||
@@ -120,22 +136,23 @@ public class WithdrawalServiceImpl extends ServiceImpl<WithdrawalRequestMapper,
|
|||||||
public void markManualSuccess(String requestId, String operatorBy) {
|
public void markManualSuccess(String requestId, String operatorBy) {
|
||||||
WithdrawalRequestEntity req = this.getById(requestId);
|
WithdrawalRequestEntity req = this.getById(requestId);
|
||||||
if (req == null) throw new CustomException("请求不存在");
|
if (req == null) throw new CustomException("请求不存在");
|
||||||
if (!"pending".equals(req.getStatus()) && !"processing".equals(req.getStatus())) {
|
if (!WithdrawalRequestStatus.PENDING.getCode().equals(req.getStatus())
|
||||||
|
&& !WithdrawalRequestStatus.PROCESSING.getCode().equals(req.getStatus())) {
|
||||||
throw new CustomException("当前状态不可操作");
|
throw new CustomException("当前状态不可操作");
|
||||||
}
|
}
|
||||||
WithdrawalRequestEntity update = new WithdrawalRequestEntity();
|
WithdrawalRequestEntity update = new WithdrawalRequestEntity();
|
||||||
update.setId(req.getId());
|
update.setId(req.getId());
|
||||||
update.setStatus("success");
|
update.setStatus(WithdrawalRequestStatus.SUCCESS.getCode());
|
||||||
this.updateById(update);
|
this.updateById(update);
|
||||||
|
|
||||||
// Set reserved earnings lines to withdrawn
|
// Set reserved earnings lines to withdrawn
|
||||||
earningsService.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
|
earningsService.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
|
||||||
.eq(EarningsLineEntity::getWithdrawalId, req.getId())
|
.eq(EarningsLineEntity::getWithdrawalId, req.getId())
|
||||||
.eq(EarningsLineEntity::getStatus, "withdrawing")
|
.eq(EarningsLineEntity::getStatus, EarningsLineStatus.WITHDRAWING.getCode())
|
||||||
.set(EarningsLineEntity::getStatus, "withdrawn"));
|
.set(EarningsLineEntity::getStatus, EarningsLineStatus.WITHDRAWN.getCode()));
|
||||||
|
|
||||||
withdrawalLogService.log(req.getTenantId(), req.getClerkId(), req.getId(),
|
withdrawalLogService.log(req.getTenantId(), req.getClerkId(), req.getId(),
|
||||||
"PAYOUT_SUCCESS", req.getStatus(), "success",
|
"PAYOUT_SUCCESS", req.getStatus(), WithdrawalRequestStatus.SUCCESS.getCode(),
|
||||||
"手动打款成功,操作人=" + operatorBy, null);
|
"手动打款成功,操作人=" + operatorBy, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,16 +161,16 @@ public class WithdrawalServiceImpl extends ServiceImpl<WithdrawalRequestMapper,
|
|||||||
public void autoPayout(String requestId) {
|
public void autoPayout(String requestId) {
|
||||||
WithdrawalRequestEntity req = this.getById(requestId);
|
WithdrawalRequestEntity req = this.getById(requestId);
|
||||||
if (req == null) throw new CustomException("请求不存在");
|
if (req == null) throw new CustomException("请求不存在");
|
||||||
if (!"pending".equals(req.getStatus())) {
|
if (!WithdrawalRequestStatus.PENDING.getCode().equals(req.getStatus())) {
|
||||||
throw new CustomException("当前状态不可自动打款");
|
throw new CustomException("当前状态不可自动打款");
|
||||||
}
|
}
|
||||||
// Transition to processing and log
|
// Transition to processing and log
|
||||||
WithdrawalRequestEntity update = new WithdrawalRequestEntity();
|
WithdrawalRequestEntity update = new WithdrawalRequestEntity();
|
||||||
update.setId(req.getId());
|
update.setId(req.getId());
|
||||||
update.setStatus("processing");
|
update.setStatus(WithdrawalRequestStatus.PROCESSING.getCode());
|
||||||
this.updateById(update);
|
this.updateById(update);
|
||||||
withdrawalLogService.log(req.getTenantId(), req.getClerkId(), req.getId(),
|
withdrawalLogService.log(req.getTenantId(), req.getClerkId(), req.getId(),
|
||||||
"PAYOUT_REQUESTED", req.getStatus(), "processing",
|
"PAYOUT_REQUESTED", req.getStatus(), WithdrawalRequestStatus.PROCESSING.getCode(),
|
||||||
"发起支付宝打款(未实现)", null);
|
"发起支付宝打款(未实现)", null);
|
||||||
|
|
||||||
// Not implemented yet
|
// Not implemented yet
|
||||||
@@ -165,27 +182,30 @@ public class WithdrawalServiceImpl extends ServiceImpl<WithdrawalRequestMapper,
|
|||||||
public void reject(String requestId, String reason) {
|
public void reject(String requestId, String reason) {
|
||||||
WithdrawalRequestEntity req = this.getById(requestId);
|
WithdrawalRequestEntity req = this.getById(requestId);
|
||||||
if (req == null) throw new CustomException("请求不存在");
|
if (req == null) throw new CustomException("请求不存在");
|
||||||
if ("success".equals(req.getStatus())) {
|
if (WithdrawalRequestStatus.SUCCESS.getCode().equals(req.getStatus())) {
|
||||||
throw new CustomException("已成功的提现不可拒绝");
|
throw new CustomException("已成功的提现不可拒绝");
|
||||||
}
|
}
|
||||||
if ("canceled".equals(req.getStatus()) || "rejected".equals(req.getStatus())) {
|
if (WithdrawalRequestStatus.CANCELED.getCode().equals(req.getStatus())
|
||||||
|
|| WithdrawalRequestStatus.REJECTED.getCode().equals(req.getStatus())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
WithdrawalRequestEntity update = new WithdrawalRequestEntity();
|
WithdrawalRequestEntity update = new WithdrawalRequestEntity();
|
||||||
update.setId(req.getId());
|
update.setId(req.getId());
|
||||||
update.setStatus("canceled");
|
update.setStatus(WithdrawalRequestStatus.CANCELED.getCode());
|
||||||
update.setFailureReason(StringUtils.hasText(reason) ? reason : "rejected");
|
update.setFailureReason(StringUtils.hasText(reason) ? reason : "rejected");
|
||||||
this.updateById(update);
|
this.updateById(update);
|
||||||
|
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
List<EarningsLineEntity> lines = earningsService.lambdaQuery()
|
||||||
.eq(EarningsLineEntity::getWithdrawalId, req.getId())
|
.eq(EarningsLineEntity::getWithdrawalId, req.getId())
|
||||||
.eq(EarningsLineEntity::getStatus, "withdrawing")
|
.eq(EarningsLineEntity::getStatus, EarningsLineStatus.WITHDRAWING.getCode())
|
||||||
.list();
|
.list();
|
||||||
for (EarningsLineEntity line : lines) {
|
for (EarningsLineEntity line : lines) {
|
||||||
LocalDateTime unlock = line.getUnlockTime();
|
LocalDateTime unlock = line.getUnlockTime();
|
||||||
String restored = unlock != null && unlock.isAfter(now) ? "frozen" : "available";
|
String restored = unlock != null && unlock.isAfter(now)
|
||||||
|
? EarningsLineStatus.FROZEN.getCode()
|
||||||
|
: EarningsLineStatus.AVAILABLE.getCode();
|
||||||
earningsService.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
|
earningsService.update(Wrappers.lambdaUpdate(EarningsLineEntity.class)
|
||||||
.eq(EarningsLineEntity::getId, line.getId())
|
.eq(EarningsLineEntity::getId, line.getId())
|
||||||
.set(EarningsLineEntity::getWithdrawalId, null)
|
.set(EarningsLineEntity::getWithdrawalId, null)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import io.swagger.annotations.ApiModelProperty;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
@@ -23,4 +24,26 @@ public class ClerkWithdrawBalanceVo {
|
|||||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||||
private LocalDateTime nextUnlockAt;
|
private LocalDateTime nextUnlockAt;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否提现锁定(仅允许同时存在一笔在途申请)")
|
||||||
|
private Boolean withdrawLocked;
|
||||||
|
|
||||||
|
@ApiModelProperty("锁定原因")
|
||||||
|
private String withdrawLockReason;
|
||||||
|
|
||||||
|
@ApiModelProperty("当前在途申请(pending/processing)")
|
||||||
|
private ActiveRequest activeRequest;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class ActiveRequest {
|
||||||
|
@ApiModelProperty("申请金额")
|
||||||
|
private BigDecimal amount;
|
||||||
|
@ApiModelProperty("状态 pending/processing")
|
||||||
|
private String status;
|
||||||
|
@ApiModelProperty("提交时间 yyyy-MM-dd HH:mm:ss")
|
||||||
|
private String createdTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.starry.admin.api;
|
package com.starry.admin.api;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.anyOf;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||||
@@ -123,7 +125,7 @@ class AdminEarningsAdjustmentControllerApiTest extends AbstractApiTest {
|
|||||||
.andExpect(jsonPath("$.code").value(200))
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
.andExpect(jsonPath("$.data.idempotencyKey").value(key))
|
.andExpect(jsonPath("$.data.idempotencyKey").value(key))
|
||||||
.andExpect(jsonPath("$.data.adjustmentId").isNotEmpty())
|
.andExpect(jsonPath("$.data.adjustmentId").isNotEmpty())
|
||||||
.andExpect(jsonPath("$.data.status").value("PROCESSING"));
|
.andExpect(jsonPath("$.data.status").value(anyOf(is("PROCESSING"), is("APPLIED"))));
|
||||||
|
|
||||||
// After implementation, the system should eventually transition to APPLIED.
|
// After implementation, the system should eventually transition to APPLIED.
|
||||||
// Poll with a bounded wait to keep the test deterministic.
|
// Poll with a bounded wait to keep the test deterministic.
|
||||||
|
|||||||
@@ -107,11 +107,13 @@ class AdminEarningsDeductionBatchAuthorizationApiTest extends AbstractApiTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void previewWithoutPermissionReturns403() throws Exception {
|
void previewWithoutPermissionReturns403() throws Exception {
|
||||||
String payload = "{\"beginTime\":\"2026-01-01 00:00:00\",\"endTime\":\"2026-01-07 23:59:59\"," +
|
String payload = "{\"clerkIds\":[\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"]," +
|
||||||
|
"\"beginTime\":\"2026-01-01 00:00:00\",\"endTime\":\"2026-01-07 23:59:59\"," +
|
||||||
"\"ruleType\":\"FIXED\",\"amount\":\"10.00\",\"operation\":\"BONUS\",\"reasonDescription\":\"x\"}";
|
"\"ruleType\":\"FIXED\",\"amount\":\"10.00\",\"operation\":\"BONUS\",\"reasonDescription\":\"x\"}";
|
||||||
mockMvc.perform(post(BASE_URL + "/preview")
|
mockMvc.perform(post(BASE_URL + "/preview")
|
||||||
.header(USER_HEADER, "leader-no-perm")
|
.header(USER_HEADER, "leader-no-perm")
|
||||||
.header(TENANT_HEADER, DEFAULT_TENANT)
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(PERMISSIONS_HEADER, "nope")
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content(payload))
|
.content(payload))
|
||||||
.andExpect(status().isForbidden());
|
.andExpect(status().isForbidden());
|
||||||
@@ -296,17 +298,20 @@ class AdminEarningsDeductionBatchAuthorizationApiTest extends AbstractApiTest {
|
|||||||
// Contract: read endpoints are protected even if the resource does not exist.
|
// Contract: read endpoints are protected even if the resource does not exist.
|
||||||
mockMvc.perform(get(BASE_URL + "/idempotency/" + UUID.randomUUID())
|
mockMvc.perform(get(BASE_URL + "/idempotency/" + UUID.randomUUID())
|
||||||
.header(USER_HEADER, "read-no-perm")
|
.header(USER_HEADER, "read-no-perm")
|
||||||
.header(TENANT_HEADER, DEFAULT_TENANT))
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(PERMISSIONS_HEADER, "nope"))
|
||||||
.andExpect(status().isForbidden());
|
.andExpect(status().isForbidden());
|
||||||
|
|
||||||
mockMvc.perform(get(BASE_URL + "/" + UUID.randomUUID() + "/items")
|
mockMvc.perform(get(BASE_URL + "/" + UUID.randomUUID() + "/items")
|
||||||
.header(USER_HEADER, "read-no-perm")
|
.header(USER_HEADER, "read-no-perm")
|
||||||
.header(TENANT_HEADER, DEFAULT_TENANT))
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(PERMISSIONS_HEADER, "nope"))
|
||||||
.andExpect(status().isForbidden());
|
.andExpect(status().isForbidden());
|
||||||
|
|
||||||
mockMvc.perform(get(BASE_URL + "/" + UUID.randomUUID() + "/logs")
|
mockMvc.perform(get(BASE_URL + "/" + UUID.randomUUID() + "/logs")
|
||||||
.header(USER_HEADER, "read-no-perm")
|
.header(USER_HEADER, "read-no-perm")
|
||||||
.header(TENANT_HEADER, DEFAULT_TENANT))
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(PERMISSIONS_HEADER, "nope"))
|
||||||
.andExpect(status().isForbidden());
|
.andExpect(status().isForbidden());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,76 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
|||||||
.isEqualTo(now.plusHours(6).format(DATE_TIME_FORMATTER));
|
.isEqualTo(now.plusHours(6).format(DATE_TIME_FORMATTER));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void balanceIndicatesWithdrawLockedWhenPendingRequestExists__covers_WD_001() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
WithdrawalRequestEntity req = new WithdrawalRequestEntity();
|
||||||
|
String requestId = IdUtils.getUuid();
|
||||||
|
req.setId(requestId);
|
||||||
|
req.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
|
||||||
|
req.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
req.setAmount(new BigDecimal("10.00"));
|
||||||
|
req.setFee(BigDecimal.ZERO);
|
||||||
|
req.setNetAmount(new BigDecimal("10.00"));
|
||||||
|
req.setStatus("pending");
|
||||||
|
req.setCreatedTime(new Date());
|
||||||
|
withdrawalService.save(req);
|
||||||
|
withdrawalsToCleanup.add(requestId);
|
||||||
|
|
||||||
|
MvcResult result = mockMvc.perform(get("/wx/withdraw/balance")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
JsonNode root = objectMapper.readTree(result.getResponse().getContentAsString());
|
||||||
|
JsonNode data = root.get("data");
|
||||||
|
assertThat(data.path("withdrawLocked").asBoolean()).isTrue();
|
||||||
|
assertThat(data.path("withdrawLockReason").asText()).isNotBlank();
|
||||||
|
JsonNode active = data.path("activeRequest");
|
||||||
|
assertThat(active.isMissingNode() || active.isNull()).isFalse();
|
||||||
|
assertThat(active.path("status").asText()).isEqualTo("pending");
|
||||||
|
assertThat(active.path("amount").decimalValue()).isEqualByComparingTo("10.00");
|
||||||
|
assertThat(active.path("createdTime").asText()).isNotBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void balanceDoesNotLockWhenActiveWithdrawalBelongsToDifferentTenant__covers_WD_001() throws Exception {
|
||||||
|
ensureTenantContext();
|
||||||
|
String otherTenantId = ApiTestDataSeeder.DEFAULT_TENANT_ID + "-other";
|
||||||
|
String requestId = IdUtils.getUuid();
|
||||||
|
|
||||||
|
try {
|
||||||
|
SecurityUtils.setTenantId(otherTenantId);
|
||||||
|
WithdrawalRequestEntity req = new WithdrawalRequestEntity();
|
||||||
|
req.setId(requestId);
|
||||||
|
req.setTenantId(otherTenantId);
|
||||||
|
req.setClerkId(ApiTestDataSeeder.DEFAULT_CLERK_ID);
|
||||||
|
req.setAmount(new BigDecimal("10.00"));
|
||||||
|
req.setFee(BigDecimal.ZERO);
|
||||||
|
req.setNetAmount(new BigDecimal("10.00"));
|
||||||
|
req.setStatus("pending");
|
||||||
|
req.setCreatedTime(new Date());
|
||||||
|
withdrawalService.save(req);
|
||||||
|
|
||||||
|
ensureTenantContext();
|
||||||
|
mockMvc.perform(get("/wx/withdraw/balance")
|
||||||
|
.header(USER_HEADER, DEFAULT_USER)
|
||||||
|
.header(TENANT_HEADER, DEFAULT_TENANT)
|
||||||
|
.header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.code").value(200))
|
||||||
|
.andExpect(jsonPath("$.data.withdrawLocked").value(false))
|
||||||
|
.andExpect(jsonPath("$.data.withdrawLockReason").value(""));
|
||||||
|
} finally {
|
||||||
|
SecurityUtils.setTenantId(otherTenantId);
|
||||||
|
withdrawalService.removeById(requestId);
|
||||||
|
ensureTenantContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createWithdrawRejectsNonPositiveAmount__covers_WD_003() throws Exception {
|
void createWithdrawRejectsNonPositiveAmount__covers_WD_003() throws Exception {
|
||||||
ensureTenantContext();
|
ensureTenantContext();
|
||||||
@@ -248,14 +318,17 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
|||||||
refreshPayeeConfirmation();
|
refreshPayeeConfirmation();
|
||||||
String firstWithdrawal = createWithdraw(new BigDecimal("35"));
|
String firstWithdrawal = createWithdraw(new BigDecimal("35"));
|
||||||
assertLinesLocked(firstWithdrawal, lineIds[0], lineIds[1], lineIds[2]);
|
assertLinesLocked(firstWithdrawal, lineIds[0], lineIds[1], lineIds[2]);
|
||||||
|
markWithdrawalCompleted(firstWithdrawal);
|
||||||
|
|
||||||
refreshPayeeConfirmation();
|
refreshPayeeConfirmation();
|
||||||
String secondWithdrawal = createWithdraw(new BigDecimal("90"));
|
String secondWithdrawal = createWithdraw(new BigDecimal("90"));
|
||||||
assertLinesLocked(secondWithdrawal, lineIds[3], lineIds[4], lineIds[5]);
|
assertLinesLocked(secondWithdrawal, lineIds[3], lineIds[4], lineIds[5]);
|
||||||
|
markWithdrawalCompleted(secondWithdrawal);
|
||||||
|
|
||||||
refreshPayeeConfirmation();
|
refreshPayeeConfirmation();
|
||||||
String thirdWithdrawal = createWithdraw(new BigDecimal("135"));
|
String thirdWithdrawal = createWithdraw(new BigDecimal("135"));
|
||||||
assertLinesLocked(thirdWithdrawal, lineIds[6], lineIds[7], lineIds[8], lineIds[9]);
|
assertLinesLocked(thirdWithdrawal, lineIds[6], lineIds[7], lineIds[8], lineIds[9]);
|
||||||
|
markWithdrawalCompleted(thirdWithdrawal);
|
||||||
|
|
||||||
ensureTenantContext();
|
ensureTenantContext();
|
||||||
BigDecimal remaining = earningsService.getAvailableAmount(ApiTestDataSeeder.DEFAULT_CLERK_ID, LocalDateTime.now());
|
BigDecimal remaining = earningsService.getAvailableAmount(ApiTestDataSeeder.DEFAULT_CLERK_ID, LocalDateTime.now());
|
||||||
@@ -549,6 +622,14 @@ class WxWithdrawControllerApiTest extends AbstractApiTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void markWithdrawalCompleted(String withdrawalId) {
|
||||||
|
ensureTenantContext();
|
||||||
|
WithdrawalRequestEntity patch = new WithdrawalRequestEntity();
|
||||||
|
patch.setId(withdrawalId);
|
||||||
|
patch.setStatus("success");
|
||||||
|
withdrawalService.updateById(patch);
|
||||||
|
}
|
||||||
|
|
||||||
private Date toDate(LocalDateTime value) {
|
private Date toDate(LocalDateTime value) {
|
||||||
return Date.from(value.atZone(ZoneId.systemDefault()).toInstant());
|
return Date.from(value.atZone(ZoneId.systemDefault()).toInstant());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import org.mockito.Mock;
|
|||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class WxCustomMpServiceTest {
|
class WxCustomMpServiceTest {
|
||||||
@@ -294,6 +295,7 @@ class WxCustomMpServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void checkSubscribeThrowsWhenUserNotSubscribed__covers_MP_008() throws WxErrorException {
|
void checkSubscribeThrowsWhenUserNotSubscribed__covers_MP_008() throws WxErrorException {
|
||||||
|
ReflectionTestUtils.setField(wxCustomMpService, "subscribeCheckEnabled", true);
|
||||||
SysTenantEntity tenant = buildTenant();
|
SysTenantEntity tenant = buildTenant();
|
||||||
when(tenantService.selectSysTenantByTenantId(TENANT_ID)).thenReturn(tenant);
|
when(tenantService.selectSysTenantByTenantId(TENANT_ID)).thenReturn(tenant);
|
||||||
when(wxMpService.switchoverTo(tenant.getAppId())).thenReturn(wxMpService);
|
when(wxMpService.switchoverTo(tenant.getAppId())).thenReturn(wxMpService);
|
||||||
|
|||||||
Reference in New Issue
Block a user