package com.talentcard.server.business.portal;
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
import java.util.Map;
|
|
import com.talentcard.server.common.BusinessException;
|
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.Test;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.test.context.ActiveProfiles;
|
|
@SpringBootTest
|
@ActiveProfiles("test")
|
class AppOrderPortalServiceTests {
|
@Autowired
|
private JdbcTemplate jdbc;
|
|
@Autowired
|
private AppOrderPortalService appOrderPortalService;
|
|
private Long talentId;
|
private Long merchantId;
|
private Long productId;
|
private Long couponId;
|
private Long onboardingId;
|
|
@BeforeEach
|
void prepareData() {
|
String suffix = String.valueOf(System.nanoTime());
|
jdbc.update("insert into talent_user(openid, name, mobile, status, deleted) values (?, ?, ?, 1, 0)", "app-order-" + suffix, "App order user", "13900000000");
|
talentId = jdbc.queryForObject("select max(id) from talent_user", Long.class);
|
jdbc.update("insert into talent_card(talent_id, card_no, card_type, card_level, status, deleted) values (?, ?, 'PERSONAL', ?, 1, 0)", talentId, "TC-APP-" + suffix, "卓越人才卡");
|
jdbc.update("insert into merchant(merchant_name, contact_mobile, address, status, deleted) values (?, ?, ?, 1, 0)", "App order merchant " + suffix, "08120000000", "Test address");
|
merchantId = jdbc.queryForObject("select max(id) from merchant", Long.class);
|
// A merchant may only take paid orders once onboarding is approved and settlement capabilities are confirmed.
|
onboardingId = readyMerchant(merchantId, "app-order-" + suffix);
|
jdbc.update("""
|
insert into merchant_product(product_no, merchant_id, product_type, product_name, original_price, sale_price,
|
subsidy_amount, stock, audit_status, status, deleted)
|
values (?, ?, 'food', 'Test package', 100.00, 60.00, 0.00, 10, 2, 1, 0)
|
""", "APP-ORDER-PRODUCT-" + suffix, merchantId);
|
productId = jdbc.queryForObject("select max(id) from merchant_product", Long.class);
|
jdbc.update("""
|
insert into merchant_coupon(merchant_id, coupon_name, condition_type, coupon_amount, product_scope, status, deleted)
|
values (?, 'Test coupon', 'NONE', 10.00, 'ALL', 1, 0)
|
""", merchantId);
|
couponId = jdbc.queryForObject("select max(id) from merchant_coupon", Long.class);
|
}
|
|
/** Creates a store, an approved onboarding and an approved settlement card for the merchant. */
|
private Long readyMerchant(Long merchant, String suffix) {
|
jdbc.update("insert into merchant_store(merchant_id, store_name, status, deleted) values (?, ?, 1, 0)", merchant, "App order store " + suffix);
|
Long storeId = jdbc.queryForObject("select max(id) from merchant_store", Long.class);
|
jdbc.update("""
|
insert into merchant_onboarding_application(merchant_id, store_id, application_no, subject_type, subject_name,
|
contact_name, contact_mobile, idempotency_key, status, provider_huifu_id,
|
auto_settle_status, cash_switch_status, merchant_stat, capability_verified_at)
|
values (?, ?, ?, 'individual', 'App order owner', 'App order owner', '13900000000', ?, 'approved', ?, '0', '1', 'NORMAL', current_timestamp)
|
""", merchant, storeId, "MO-APP-" + suffix, "app-order-onboarding-" + suffix, shortReference("H", suffix));
|
Long applicationId = jdbc.queryForObject("select max(id) from merchant_onboarding_application", Long.class);
|
jdbc.update("""
|
insert into merchant_bank_card(merchant_id, store_id, bank_name, branch_name, city_code, card_tail, card_masked,
|
card_hash, status, idempotency_key, provider_token_no, reviewed_at)
|
values (?, ?, '中国建设银行', '攀枝花东区支行', '510400', '8888', '**** **** **** 8888', ?, 'approved', ?, ?, current_timestamp)
|
""", merchant, storeId, "hash-app-order-" + suffix, "app-order-card-" + suffix, shortReference("T", suffix));
|
Long cardId = jdbc.queryForObject("select max(id) from merchant_bank_card", Long.class);
|
jdbc.update("update merchant_onboarding_application set initial_bank_card_id=? where id=?", cardId, applicationId);
|
// 收款就绪要求微信支付业务开通与实名/授权都有通道证据。这里的商户建模成已经办完这两步,
|
// 否则下单会被「微信收款能力未就绪」挡住——那些是开通流程的测试,不是下单的测试。
|
jdbc.update("insert into merchant_onboarding_progress(application_id, stages_json) values (?, '{\"wechat\":\"confirmed\",\"realname\":\"confirmed\"}')", applicationId);
|
return applicationId;
|
}
|
|
/** Provider identifiers are limited to 32 characters, so tests keep them short. */
|
private String shortReference(String prefix, String suffix) {
|
return prefix + suffix.substring(Math.max(0, suffix.length() - 8));
|
}
|
|
@Test
|
void createsPendingOrderFromServerSideProductAndCouponSnapshot() {
|
Map<String, Object> created = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, couponId), "app-order-test-" + System.nanoTime());
|
|
assertThat(created.get("orderNo")).asString().startsWith("P");
|
assertThat(created).containsEntry("talentId", talentId).containsEntry("merchantId", merchantId).containsEntry("productId", productId);
|
Map<String, Object> row = jdbc.queryForMap("""
|
select order_amount as orderAmount, base_pay_amount as basePayAmount, coupon_amount as couponAmount,
|
pay_amount as payAmount, order_status as orderStatus, order_type as orderType,
|
product_name_snapshot as productNameSnapshot
|
from benefit_order
|
where order_no = ?
|
""", created.get("orderNo"));
|
assertThat(row).containsEntry("ORDERAMOUNT", 10000L)
|
.containsEntry("BASEPAYAMOUNT", 6000L)
|
.containsEntry("COUPONAMOUNT", 1000L)
|
.containsEntry("PAYAMOUNT", 5000L)
|
.containsEntry("ORDERSTATUS", 0)
|
.containsEntry("ORDERTYPE", "GROUP_BUY");
|
assertThat(row.get("PRODUCTNAMESNAPSHOT")).isEqualTo("Test package");
|
}
|
|
@Test
|
void reusesOrderForSameIdempotencyKey() {
|
String idempotencyKey = "app-order-repeat-" + System.nanoTime();
|
Map<String, Object> first = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), idempotencyKey);
|
Map<String, Object> second = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), idempotencyKey);
|
|
assertThat(second.get("orderNo")).isEqualTo(first.get("orderNo"));
|
}
|
|
@Test
|
void cancelsExpiredPendingOrderBeforeReusingItsCoupon() {
|
Map<String, Object> expired = appOrderPortalService.create(
|
talentId,
|
new AppOrderCreateRequest(merchantId, productId, couponId),
|
"app-order-expired-first-" + System.nanoTime());
|
jdbc.update("update benefit_order set expires_at = current_timestamp - interval '1' minute where order_no = ?", expired.get("orderNo"));
|
|
Map<String, Object> replacement = appOrderPortalService.create(
|
talentId,
|
new AppOrderCreateRequest(merchantId, productId, couponId),
|
"app-order-expired-replacement-" + System.nanoTime());
|
|
assertThat(replacement.get("orderNo")).isNotEqualTo(expired.get("orderNo"));
|
assertThat(jdbc.queryForObject("select order_status from benefit_order where order_no = ?", Integer.class, expired.get("orderNo"))).isEqualTo(2);
|
assertThat(jdbc.queryForObject("select coupon_id from benefit_order where order_no = ?", Long.class, replacement.get("orderNo"))).isEqualTo(couponId);
|
}
|
|
/** 自动结算开着不再阻止下单:未核销的钱由延迟户兜住,不会被自动结算提前结走。 */
|
@Test
|
void allowsPaidOrderEvenWhenMerchantAutoSettlementIsOn() {
|
jdbc.update("update merchant_onboarding_application set auto_settle_status='1' where id=?", onboardingId);
|
|
Map<String, Object> created = appOrderPortalService.create(talentId,
|
new AppOrderCreateRequest(merchantId, productId, null), "app-order-auto-settle-" + System.nanoTime());
|
assertThat(created.get("orderNo")).isNotNull();
|
}
|
|
@Test
|
void refusesPaidOrderWhenMerchantOnboardingIsNotApproved() {
|
jdbc.update("update merchant_onboarding_application set status='pending', capability_verified_at=null where id=?", onboardingId);
|
jdbc.update("update merchant_bank_card set status='pending', provider_token_no=null where merchant_id=?", merchantId);
|
|
assertThatThrownBy(() -> appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), "app-order-pending-" + System.nanoTime()))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("进件");
|
}
|
|
@Test
|
void fixesTheCollectingSubMerchantOnThePaidOrder() {
|
Map<String, Object> created = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), "app-order-collector-" + System.nanoTime());
|
|
Map<String, Object> onboarding = jdbc.queryForMap("select store_id, provider_huifu_id from merchant_onboarding_application where id = ?", onboardingId);
|
Map<String, Object> order = jdbc.queryForMap("""
|
select collect_onboarding_id, collect_store_id, collect_huifu_id
|
from benefit_order where order_no = ?
|
""", created.get("orderNo"));
|
assertThat(order).containsEntry("collect_onboarding_id", onboardingId)
|
.containsEntry("collect_store_id", onboarding.get("store_id"))
|
.containsEntry("collect_huifu_id", onboarding.get("provider_huifu_id"));
|
}
|
|
@Test
|
void leavesTheCollectingSubMerchantEmptyOnAFreeOrder() {
|
jdbc.update("""
|
update merchant_product
|
set rights_config = ?
|
where id = ?
|
""", "[{\"level\":\"卓越人才卡\",\"enabled\":true,\"free\":true,\"limitCount\":1,\"exceedDiscount\":6,\"resetCycle\":\"每月\"}]", productId);
|
|
Map<String, Object> freeOrder = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), "app-order-free-collector-" + System.nanoTime());
|
|
Map<String, Object> order = jdbc.queryForMap("""
|
select pay_amount, collect_onboarding_id, collect_huifu_id
|
from benefit_order where order_no = ?
|
""", freeOrder.get("orderNo"));
|
assertThat(order).containsEntry("pay_amount", 0L)
|
.containsEntry("collect_onboarding_id", null)
|
.containsEntry("collect_huifu_id", null);
|
}
|
|
@Test
|
void refusesPaidOrderWhenTheApprovedOnboardingHasNoHuifuId() {
|
jdbc.update("update merchant_onboarding_application set provider_huifu_id=null where id=?", onboardingId);
|
|
assertThatThrownBy(() -> appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), "app-order-no-huifu-" + System.nanoTime()))
|
.isInstanceOf(BusinessException.class)
|
.hasMessageContaining("斗拱商户号");
|
}
|
|
@Test
|
void usesAvailableFreeCountBeforeChargingTheProductPrice() {
|
jdbc.update("""
|
update merchant_product
|
set rights_config = ?
|
where id = ?
|
""", "[{\"level\":\"卓越人才卡\",\"enabled\":true,\"free\":true,\"limitCount\":1,\"exceedDiscount\":6,\"resetCycle\":\"每月\"}]", productId);
|
|
Map<String, Object> freeOrder = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), "app-order-free-" + System.nanoTime());
|
Long freePayAmount = jdbc.queryForObject("select pay_amount from benefit_order where order_no = ?", Long.class, freeOrder.get("orderNo"));
|
assertThat(freePayAmount).isZero();
|
|
Map<String, Object> paidOrder = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), "app-order-after-free-" + System.nanoTime());
|
Long paidAmount = jdbc.queryForObject("select pay_amount from benefit_order where order_no = ?", Long.class, paidOrder.get("orderNo"));
|
assertThat(paidAmount).isEqualTo(6000L);
|
}
|
}
|