package com.talentcard.server.business.merchant; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.math.BigDecimal; import java.util.List; import java.util.Map; import com.talentcard.server.business.PageResult; import com.talentcard.server.business.merchant.auth.MerchantPrincipal; 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 MerchantManagementServiceTests { @Autowired private JdbcTemplate jdbc; @Autowired private MerchantManagementService service; private Long merchantId; private Long storeId; private MerchantPrincipal principal; @BeforeEach void prepareMerchant() { String suffix = String.valueOf(System.nanoTime()); jdbc.update("insert into merchant(merchant_name, address, business_status, status, can_login_merchant_app, deleted) values (?, ?, 'OPEN', 1, 1, 0)", "Management test merchant " + suffix, "测试商户地址"); merchantId = jdbc.queryForObject("select max(id) from merchant", Long.class); jdbc.update("insert into merchant_store(merchant_id, store_name, address, status, deleted) values (?, ?, ?, 1, 0)", merchantId, "测试门店", "测试门店地址"); storeId = jdbc.queryForObject("select max(id) from merchant_store", Long.class); principal = new MerchantPrincipal(1000L, merchantId, storeId, "operator"); insertProduct("ONLINE", 1, 2); insertProduct("OFFLINE", 0, 2); insertProduct("PENDING", 0, 1); } @Test void returnsMerchantScopedProfileAndProducts() { Map profile = service.profile(principal); PageResult> online = service.products(principal, "online", 1, 10); PageResult> offline = service.products(principal, "offline", 1, 10); PageResult> review = service.products(principal, "review", 1, 10); assertThat(profile).containsEntry("address", "测试门店地址").containsEntry("businessStatus", "open").containsEntry("onlineProductCount", 1L); assertThat(online.total()).isEqualTo(1); assertThat(offline.total()).isEqualTo(1); assertThat(review.total()).isEqualTo(3); } @Test void updatesBusinessHoursAndRejectsOverlaps() { Map saved = service.updateBusinessHours(principal, new MerchantBusinessHoursRequest(List.of( new MerchantBusinessHoursRequest.Period("08:00", "12:00"), new MerchantBusinessHoursRequest.Period("14:00", "18:00") ))); assertThat(saved.get("periods")).asList().hasSize(2); assertThatThrownBy(() -> service.updateBusinessHours(principal, new MerchantBusinessHoursRequest(List.of( new MerchantBusinessHoursRequest.Period("08:00", "12:00"), new MerchantBusinessHoursRequest.Period("11:00", "18:00") )))).isInstanceOf(BusinessException.class); } @Test void updatingProductResubmitsForReviewAndPreventsPendingProductFromGoingOnline() { Long productId = jdbc.queryForObject("select id from merchant_product where merchant_id = ? and product_no like 'MANAGEMENT-ONLINE-%'", Long.class, merchantId); Map updated = service.updateProduct(principal, productId, new MerchantProductUpdateRequest( "更新后的商品", new BigDecimal("99.90"), "https://example.com/product.jpg", List.of("https://example.com/product-1.jpg"), "更新后的图文详情", Map.of("validity", "30天", "usage", "到店核销") )); assertThat(updated).containsEntry("status", "offline").containsEntry("auditStatus", "pending"); assertThatThrownBy(() -> service.updateProductStatus(principal, productId, new MerchantProductStatusRequest("online"))) .isInstanceOf(BusinessException.class); } /** * 体检医院在后台团单里只有「商品名称 / 封面图 / 可预约时间 / 检查项目」,没有商品金额。 * 商家端照这套字段保存时:可预约时间要落到 purchase_notice.validPeriod,金额允许为空并按 0(免费)落库。 */ @Test void savesHealthCheckProductWithoutPriceAndKeepsReservationWindow() { jdbc.update("update merchant set merchant_type_code = 'medical' where id = ?", merchantId); Long productId = jdbc.queryForObject("select id from merchant_product where merchant_id = ? and product_no like 'MANAGEMENT-ONLINE-%'", Long.class, merchantId); Map updated = service.updateProduct(principal, productId, new MerchantProductUpdateRequest( "查血", null, "https://example.com/cover.jpg", List.of(), "[{\"name\":\"查血\",\"meaning\":\"检查血脂\",\"gender\":[\"男\",\"女\"]}]", Map.of("validPeriod", "2026-09-16 至 2026-09-18") )); assertThat(updated).containsEntry("merchantTypeCode", "medical"); assertThat((BigDecimal) updated.get("salePrice")).isEqualByComparingTo(BigDecimal.ZERO); assertThat((Map) updated.get("purchaseNotice")) .containsEntry("validPeriod", "2026-09-16 至 2026-09-18"); Map detail = service.product(principal, productId); assertThat(String.valueOf(detail.get("detailText"))).contains("检查血脂"); } /** 非体检医院仍然要有商品金额:商家端少了金额字段时不能静默按免费存下去。 */ @Test void rejectsMissingPriceForNonHealthCheckMerchant() { Long productId = jdbc.queryForObject("select id from merchant_product where merchant_id = ? and product_no like 'MANAGEMENT-ONLINE-%'", Long.class, merchantId); assertThatThrownBy(() -> service.updateProduct(principal, productId, new MerchantProductUpdateRequest( "普通商品", null, "https://example.com/cover.jpg", List.of("https://example.com/product-1.jpg"), "图文详情", Map.of() ))).isInstanceOf(BusinessException.class).hasMessageContaining("商品金额"); } private void insertProduct(String marker, int status, int auditStatus) { jdbc.update(""" insert into merchant_product(merchant_id, product_no, product_type, product_name, cover_url, original_price, sale_price, audit_status, status, submit_time, deleted) values (?, ?, 'food', ?, '/static/test.jpg', 100.00, 80.00, ?, ?, now(), 0) """, merchantId, "MANAGEMENT-" + marker + "-" + System.nanoTime(), marker + " product", auditStatus, status); } }