CouponServiceTest.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. <?php
  2. namespace Tests\Unit;
  3. use App\Models\UserCoupon;
  4. use App\Services\CouponService;
  5. use Tests\TestCase;
  6. use Illuminate\Support\Facades\DB;
  7. /**
  8. * 优惠券服务单元测试
  9. *
  10. * 测试 CouponService 的核心业务逻辑:
  11. * - 优惠券发放条件判断
  12. * - 优惠券验证
  13. * - 金币计算
  14. * - 优惠券使用
  15. */
  16. class CouponServiceTest extends TestCase
  17. {
  18. /** @var CouponService */
  19. protected $service;
  20. /** @var int 测试用户ID */
  21. protected $testUserId = 999999;
  22. public function setUp(): void
  23. {
  24. parent::setUp();
  25. $this->service = new CouponService();
  26. // 清理测试数据
  27. DB::connection('write')->table(UserCoupon::TABLE)
  28. ->where('user_id', $this->testUserId)
  29. ->delete();
  30. }
  31. public function tearDown(): void
  32. {
  33. // 清理测试数据
  34. DB::connection('write')->table(UserCoupon::TABLE)
  35. ->where('user_id', $this->testUserId)
  36. ->delete();
  37. parent::tearDown();
  38. }
  39. // ========================
  40. // 固定金额券 - 金币计算
  41. // ========================
  42. /**
  43. * 测试固定金额券:赠送金额等于券面值
  44. */
  45. public function test_calc_bonus_coins_fixed_type()
  46. {
  47. $coupon = (object) [
  48. 'coupon_type' => UserCoupon::TYPE_FIXED,
  49. 'coupon_value' => 10.00, // 10元
  50. 'max_bonus' => 0,
  51. ];
  52. $bonus = $this->service->calcBonusCoins($coupon, 50.00);
  53. $this->assertEquals(1000, $bonus); // 10元 × 100 = 1000分
  54. }
  55. /**
  56. * 测试固定金额券:不同充值金额,赠送不变
  57. */
  58. public function test_calc_bonus_coins_fixed_independent_of_pay_amt()
  59. {
  60. $coupon = (object) [
  61. 'coupon_type' => UserCoupon::TYPE_FIXED,
  62. 'coupon_value' => 5.00,
  63. 'max_bonus' => 0,
  64. ];
  65. $bonus1 = $this->service->calcBonusCoins($coupon, 10.00);
  66. $bonus2 = $this->service->calcBonusCoins($coupon, 100.00);
  67. $this->assertEquals($bonus1, $bonus2); // 固定金额与充值额无关
  68. $this->assertEquals(500, $bonus1);
  69. }
  70. // ========================
  71. // 百分比券 - 金币计算
  72. // ========================
  73. /**
  74. * 测试百分比券:按充值金额百分比计算
  75. */
  76. public function test_calc_bonus_coins_percent_type()
  77. {
  78. $coupon = (object) [
  79. 'coupon_type' => UserCoupon::TYPE_PERCENT,
  80. 'coupon_value' => 50, // 50%
  81. 'max_bonus' => 100, // 上限100元
  82. ];
  83. $bonus = $this->service->calcBonusCoins($coupon, 40.00);
  84. $this->assertEquals(2000, $bonus); // 40 × 50% × 100 = 2000分
  85. }
  86. /**
  87. * 测试百分比券:超过上限时按上限计算
  88. */
  89. public function test_calc_bonus_coins_percent_capped_at_max()
  90. {
  91. $coupon = (object) [
  92. 'coupon_type' => UserCoupon::TYPE_PERCENT,
  93. 'coupon_value' => 50,
  94. 'max_bonus' => 30, // 上限30元
  95. ];
  96. // 充值200元,50% = 100元,但上限30元 → 3000分
  97. $bonus = $this->service->calcBonusCoins($coupon, 200.00);
  98. $this->assertEquals(3000, $bonus); // 30 × 100 = 3000分
  99. }
  100. // ========================
  101. // 优惠券验证
  102. // ========================
  103. /**
  104. * 测试验证:优惠券不存在
  105. */
  106. public function test_validate_coupon_not_found()
  107. {
  108. $result = $this->service->validateCoupon(99999, $this->testUserId, 50.00);
  109. $this->assertFalse($result['valid']);
  110. $this->assertEquals('Coupon not found', $result['error']);
  111. }
  112. /**
  113. * 测试验证:充值金额不满足最低门槛
  114. */
  115. public function test_validate_coupon_below_min_recharge()
  116. {
  117. // 手动插入一张测试券
  118. $couponId = DB::connection('write')->table(UserCoupon::TABLE)->insertGetId([
  119. 'user_id' => $this->testUserId,
  120. 'coupon_name' => 'test_fixed',
  121. 'coupon_type' => UserCoupon::TYPE_FIXED,
  122. 'coupon_value' => 10.00,
  123. 'min_recharge' => 50.00, // 最低50元
  124. 'max_bonus' => 0,
  125. 'bonus_coins' => 0,
  126. 'order_sn' => '',
  127. 'status' => UserCoupon::STATUS_UNUSED,
  128. 'issued_at' => date('Y-m-d H:i:s'),
  129. 'used_at' => null,
  130. 'expire_at' => date('Y-m-d H:i:s', strtotime('+7 days')),
  131. ]);
  132. $result = $this->service->validateCoupon($couponId, $this->testUserId, 20.00); // 20 < 50
  133. $this->assertFalse($result['valid']);
  134. $this->assertStringContainsString('Minimum recharge', $result['error']);
  135. }
  136. /**
  137. * 测试验证:优惠券已过期
  138. */
  139. public function test_validate_coupon_expired()
  140. {
  141. $couponId = DB::connection('write')->table(UserCoupon::TABLE)->insertGetId([
  142. 'user_id' => $this->testUserId,
  143. 'coupon_name' => 'test_expired',
  144. 'coupon_type' => UserCoupon::TYPE_FIXED,
  145. 'coupon_value' => 10.00,
  146. 'min_recharge' => 0,
  147. 'max_bonus' => 0,
  148. 'bonus_coins' => 0,
  149. 'order_sn' => '',
  150. 'status' => UserCoupon::STATUS_UNUSED,
  151. 'issued_at' => date('Y-m-d H:i:s', strtotime('-10 days')),
  152. 'used_at' => null,
  153. 'expire_at' => date('Y-m-d H:i:s', strtotime('-1 day')), // 已过期
  154. ]);
  155. $result = $this->service->validateCoupon($couponId, $this->testUserId, 50.00);
  156. $this->assertFalse($result['valid']);
  157. $this->assertEquals('Coupon expired', $result['error']);
  158. }
  159. /**
  160. * 测试验证:有效优惠券
  161. */
  162. public function test_validate_coupon_valid()
  163. {
  164. $couponId = DB::connection('write')->table(UserCoupon::TABLE)->insertGetId([
  165. 'user_id' => $this->testUserId,
  166. 'coupon_name' => 'test_valid',
  167. 'coupon_type' => UserCoupon::TYPE_FIXED,
  168. 'coupon_value' => 10.00,
  169. 'min_recharge' => 30.00,
  170. 'max_bonus' => 0,
  171. 'bonus_coins' => 0,
  172. 'order_sn' => '',
  173. 'status' => UserCoupon::STATUS_UNUSED,
  174. 'issued_at' => date('Y-m-d H:i:s'),
  175. 'used_at' => null,
  176. 'expire_at' => date('Y-m-d H:i:s', strtotime('+7 days')),
  177. ]);
  178. $result = $this->service->validateCoupon($couponId, $this->testUserId, 50.00);
  179. $this->assertTrue($result['valid']);
  180. }
  181. // ========================
  182. // 防重复发放
  183. // ========================
  184. /**
  185. * 测试 hasSameCoupon:有效券存在时返回 true
  186. */
  187. public function test_has_same_coupon_returns_true_when_valid_exists()
  188. {
  189. $couponName = 'test_duplicate_check';
  190. DB::connection('write')->table(UserCoupon::TABLE)->insert([
  191. 'user_id' => $this->testUserId,
  192. 'coupon_name' => $couponName,
  193. 'coupon_type' => UserCoupon::TYPE_FIXED,
  194. 'coupon_value' => 5.00,
  195. 'min_recharge' => 0,
  196. 'max_bonus' => 0,
  197. 'bonus_coins' => 0,
  198. 'order_sn' => '',
  199. 'status' => UserCoupon::STATUS_UNUSED,
  200. 'issued_at' => date('Y-m-d H:i:s'),
  201. 'used_at' => null,
  202. 'expire_at' => date('Y-m-d H:i:s', strtotime('+7 days')),
  203. ]);
  204. $this->assertTrue(UserCoupon::hasSameCoupon($this->testUserId, $couponName));
  205. }
  206. /**
  207. * 测试 hasSameCoupon:同名券已过期时返回 false(允许重新发放)
  208. */
  209. public function test_has_same_coupon_returns_false_when_expired()
  210. {
  211. $couponName = 'test_expired_dup';
  212. DB::connection('write')->table(UserCoupon::TABLE)->insert([
  213. 'user_id' => $this->testUserId,
  214. 'coupon_name' => $couponName,
  215. 'coupon_type' => UserCoupon::TYPE_FIXED,
  216. 'coupon_value' => 5.00,
  217. 'min_recharge' => 0,
  218. 'max_bonus' => 0,
  219. 'bonus_coins' => 0,
  220. 'order_sn' => '',
  221. 'status' => UserCoupon::STATUS_UNUSED,
  222. 'issued_at' => date('Y-m-d H:i:s', strtotime('-10 days')),
  223. 'used_at' => null,
  224. 'expire_at' => date('Y-m-d H:i:s', strtotime('-1 day')), // 已过期
  225. ]);
  226. $this->assertFalse(UserCoupon::hasSameCoupon($this->testUserId, $couponName));
  227. }
  228. }