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); 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 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 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 first = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), idempotencyKey); Map second = appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), idempotencyKey); assertThat(second.get("orderNo")).isEqualTo(first.get("orderNo")); } @Test void cancelsExpiredPendingOrderBeforeReusingItsCoupon() { Map 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 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 refusesPaidOrderWhileMerchantSettlementCapabilityIsMissing() { jdbc.update("update merchant_onboarding_application set auto_settle_status='1' where id=?", onboardingId); assertThatThrownBy(() -> appOrderPortalService.create(talentId, new AppOrderCreateRequest(merchantId, productId, null), "app-order-blocked-" + System.nanoTime())) .isInstanceOf(BusinessException.class) .hasMessageContaining("自动结算"); assertThat(jdbc.queryForObject("select count(*) from benefit_order where merchant_id=?", Long.class, merchantId)).isZero(); } @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 usesAvailableFreeCountBeforeChargingTheProductPrice() { jdbc.update(""" update merchant_product set rights_config = ? where id = ? """, "[{\"level\":\"卓越人才卡\",\"enabled\":true,\"free\":true,\"limitCount\":1,\"exceedDiscount\":6,\"resetCycle\":\"每月\"}]", productId); Map 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 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); } }