parent
9d125527cb
commit
4f48e61a2c
@ -126,6 +126,9 @@ class OrderController extends Controller
|
|||||||
if (!$order) {
|
if (!$order) {
|
||||||
return $this->fail([400, __('Order does not exist or has been paid')]);
|
return $this->fail([400, __('Order does not exist or has been paid')]);
|
||||||
}
|
}
|
||||||
|
if ($order->total_amount < 0) {
|
||||||
|
return $this->fail([400, __('Order amount is invalid')]);
|
||||||
|
}
|
||||||
// free process
|
// free process
|
||||||
if ($order->total_amount <= 0) {
|
if ($order->total_amount <= 0) {
|
||||||
$orderService = new OrderService($order);
|
$orderService = new OrderService($order);
|
||||||
|
|||||||
@ -36,9 +36,7 @@ class OrderHandleJob implements ShouldQueue
|
|||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
$order = Order::where('trade_no', $this->tradeNo)
|
$order = Order::where('trade_no', $this->tradeNo)->first();
|
||||||
->lockForUpdate()
|
|
||||||
->first();
|
|
||||||
if (!$order) return;
|
if (!$order) return;
|
||||||
$orderService = new OrderService($order);
|
$orderService = new OrderService($order);
|
||||||
switch ($order->status) {
|
switch ($order->status) {
|
||||||
|
|||||||
@ -15,8 +15,8 @@ use Illuminate\Support\Facades\Log;
|
|||||||
|
|
||||||
class GiftCardService
|
class GiftCardService
|
||||||
{
|
{
|
||||||
protected readonly GiftCardCode $code;
|
protected GiftCardCode $code;
|
||||||
protected readonly GiftCardTemplate $template;
|
protected GiftCardTemplate $template;
|
||||||
protected ?User $user = null;
|
protected ?User $user = null;
|
||||||
|
|
||||||
public function __construct(string $code)
|
public function __construct(string $code)
|
||||||
@ -106,6 +106,23 @@ class GiftCardService
|
|||||||
}
|
}
|
||||||
|
|
||||||
return DB::transaction(function () use ($options) {
|
return DB::transaction(function () use ($options) {
|
||||||
|
$this->code = GiftCardCode::whereKey($this->code->id)
|
||||||
|
->lockForUpdate()
|
||||||
|
->first()
|
||||||
|
?? throw new ApiException('兑换码不存在');
|
||||||
|
|
||||||
|
$this->template = GiftCardTemplate::whereKey($this->code->template_id)
|
||||||
|
->lockForUpdate()
|
||||||
|
->first()
|
||||||
|
?? throw new ApiException('该礼品卡类型不存在');
|
||||||
|
|
||||||
|
$this->user = User::whereKey($this->user->id)
|
||||||
|
->lockForUpdate()
|
||||||
|
->first()
|
||||||
|
?? throw new ApiException('用户信息未提供');
|
||||||
|
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
$actualRewards = $this->template->calculateActualRewards($this->user);
|
$actualRewards = $this->template->calculateActualRewards($this->user);
|
||||||
|
|
||||||
if ($this->template->type === GiftCardTemplate::TYPE_MYSTERY) {
|
if ($this->template->type === GiftCardTemplate::TYPE_MYSTERY) {
|
||||||
@ -198,7 +215,7 @@ class GiftCardService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$inviteUser = User::find($this->user->invite_user_id);
|
$inviteUser = User::lockForUpdate()->find($this->user->invite_user_id);
|
||||||
if (!$inviteUser) {
|
if (!$inviteUser) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -212,7 +229,9 @@ class GiftCardService
|
|||||||
if (isset($rewards['balance']) && $rewards['balance'] > 0) {
|
if (isset($rewards['balance']) && $rewards['balance'] > 0) {
|
||||||
$inviteBalance = intval($rewards['balance'] * $rate);
|
$inviteBalance = intval($rewards['balance'] * $rate);
|
||||||
if ($inviteBalance > 0) {
|
if ($inviteBalance > 0) {
|
||||||
$userService->addBalance($inviteUser->id, $inviteBalance);
|
if (!$userService->addBalance($inviteUser->id, $inviteBalance)) {
|
||||||
|
throw new ApiException('邀请人余额发放失败');
|
||||||
|
}
|
||||||
$inviteRewards['balance'] = $inviteBalance;
|
$inviteRewards['balance'] = $inviteBalance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,21 @@ class OrderService
|
|||||||
$this->order = $order;
|
$this->order = $order;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function lockCurrentOrder(): ?Order
|
||||||
|
{
|
||||||
|
return Order::whereKey($this->order->id)
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function lockCurrentOrderWhenStatus(int $status): ?Order
|
||||||
|
{
|
||||||
|
return Order::whereKey($this->order->id)
|
||||||
|
->where('status', $status)
|
||||||
|
->lockForUpdate()
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an order from a request.
|
* Create an order from a request.
|
||||||
*
|
*
|
||||||
@ -56,6 +71,15 @@ class OrderService
|
|||||||
HookManager::call('order.create.before', [$user, $plan, $period, $couponCode]);
|
HookManager::call('order.create.before', [$user, $plan, $period, $couponCode]);
|
||||||
|
|
||||||
return DB::transaction(function () use ($user, $plan, $period, $couponCode, $userService) {
|
return DB::transaction(function () use ($user, $plan, $period, $couponCode, $userService) {
|
||||||
|
$user = User::lockForUpdate()->find($user->id);
|
||||||
|
if (!$user) {
|
||||||
|
throw new ApiException(__('The user does not exist'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($userService->isNotCompleteOrderByUserId($user->id)) {
|
||||||
|
throw new ApiException(__('You have an unpaid or pending order, please try again later or cancel it'));
|
||||||
|
}
|
||||||
|
|
||||||
$newPeriod = PlanService::getPeriodKey($period);
|
$newPeriod = PlanService::getPeriodKey($period);
|
||||||
|
|
||||||
$order = new Order([
|
$order = new Order([
|
||||||
@ -79,7 +103,6 @@ class OrderService
|
|||||||
$orderService->handleUserBalance($user, $userService);
|
$orderService->handleUserBalance($user, $userService);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 余额抵扣后的 total_amount 才是用户实际支付金额,返利必须基于该金额计算。
|
|
||||||
$orderService->setInvite(user: $user);
|
$orderService->setInvite(user: $user);
|
||||||
|
|
||||||
if (!$order->save()) {
|
if (!$order->save()) {
|
||||||
@ -96,13 +119,19 @@ class OrderService
|
|||||||
|
|
||||||
public function open(): void
|
public function open(): void
|
||||||
{
|
{
|
||||||
$order = $this->order;
|
$openedOrder = DB::transaction(function () {
|
||||||
$plan = Plan::find($order->plan_id);
|
$order = $this->lockCurrentOrderWhenStatus(Order::STATUS_PROCESSING);
|
||||||
|
if (!$order) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
HookManager::call('order.open.before', $order);
|
$plan = Plan::find($order->plan_id);
|
||||||
|
if (!$plan) {
|
||||||
|
throw new \RuntimeException('订阅不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
HookManager::call('order.open.before', $order);
|
||||||
|
|
||||||
DB::transaction(function () use ($order, $plan) {
|
|
||||||
$this->user = User::lockForUpdate()->find($order->user_id);
|
$this->user = User::lockForUpdate()->find($order->user_id);
|
||||||
|
|
||||||
if ($order->surplus_credit) {
|
if ($order->surplus_credit) {
|
||||||
@ -131,10 +160,19 @@ class OrderService
|
|||||||
if (!$order->save()) {
|
if (!$order->save()) {
|
||||||
throw new \RuntimeException('订单信息保存失败');
|
throw new \RuntimeException('订单信息保存失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return $order;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!$openedOrder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order = $openedOrder;
|
||||||
|
$this->order = $order;
|
||||||
|
|
||||||
$eventId = match ((int) $order->type) {
|
$eventId = match ((int) $order->type) {
|
||||||
Order::STATUS_PROCESSING => admin_setting('new_order_event_id', 0),
|
Order::TYPE_NEW_PURCHASE => admin_setting('new_order_event_id', 0),
|
||||||
Order::TYPE_RENEWAL => admin_setting('renew_order_event_id', 0),
|
Order::TYPE_RENEWAL => admin_setting('renew_order_event_id', 0),
|
||||||
Order::TYPE_UPGRADE => admin_setting('change_order_event_id', 0),
|
Order::TYPE_UPGRADE => admin_setting('change_order_event_id', 0),
|
||||||
default => 0,
|
default => 0,
|
||||||
@ -287,16 +325,31 @@ class OrderService
|
|||||||
|
|
||||||
public function paid(string $callbackNo)
|
public function paid(string $callbackNo)
|
||||||
{
|
{
|
||||||
$order = $this->order;
|
|
||||||
if ($order->status !== Order::STATUS_PENDING)
|
|
||||||
return true;
|
|
||||||
$order->status = Order::STATUS_PROCESSING;
|
|
||||||
$order->paid_at = time();
|
|
||||||
$order->callback_no = $callbackNo;
|
|
||||||
if (!$order->save())
|
|
||||||
return false;
|
|
||||||
try {
|
try {
|
||||||
OrderHandleJob::dispatchSync($order->trade_no);
|
[$order, $shouldDispatch] = DB::transaction(function () use ($callbackNo) {
|
||||||
|
$order = $this->lockCurrentOrder();
|
||||||
|
if (!$order) {
|
||||||
|
throw new \RuntimeException('Order not found.');
|
||||||
|
}
|
||||||
|
if ((int) $order->status !== Order::STATUS_PENDING) {
|
||||||
|
return [$order, false];
|
||||||
|
}
|
||||||
|
|
||||||
|
$order->status = Order::STATUS_PROCESSING;
|
||||||
|
$order->paid_at = time();
|
||||||
|
$order->callback_no = $callbackNo;
|
||||||
|
if (!$order->save()) {
|
||||||
|
throw new \RuntimeException('Failed to save order status.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$order, true];
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->order = $order;
|
||||||
|
|
||||||
|
if ($shouldDispatch) {
|
||||||
|
OrderHandleJob::dispatchSync($order->trade_no);
|
||||||
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error($e);
|
Log::error($e);
|
||||||
return false;
|
return false;
|
||||||
@ -306,25 +359,37 @@ class OrderService
|
|||||||
|
|
||||||
public function cancel(): bool
|
public function cancel(): bool
|
||||||
{
|
{
|
||||||
$order = $this->order;
|
|
||||||
HookManager::call('order.cancel.before', $order);
|
|
||||||
try {
|
try {
|
||||||
DB::beginTransaction();
|
$cancelledOrder = DB::transaction(function () {
|
||||||
$order->status = Order::STATUS_CANCELLED;
|
$order = $this->lockCurrentOrderWhenStatus(Order::STATUS_PENDING);
|
||||||
if (!$order->save()) {
|
if (!$order) {
|
||||||
throw new \Exception('Failed to save order status.');
|
return null;
|
||||||
}
|
|
||||||
if ($order->balance_amount) {
|
|
||||||
$userService = new UserService();
|
|
||||||
if (!$userService->addBalance($order->user_id, $order->balance_amount)) {
|
|
||||||
throw new \Exception('Failed to add balance.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HookManager::call('order.cancel.before', $order);
|
||||||
|
|
||||||
|
$order->status = Order::STATUS_CANCELLED;
|
||||||
|
if (!$order->save()) {
|
||||||
|
throw new \RuntimeException('Failed to save order status.');
|
||||||
|
}
|
||||||
|
if ($order->balance_amount) {
|
||||||
|
$userService = new UserService();
|
||||||
|
if (!$userService->addBalance($order->user_id, $order->balance_amount)) {
|
||||||
|
throw new \RuntimeException('Failed to add balance.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $order;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!$cancelledOrder) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
DB::commit();
|
|
||||||
HookManager::call('order.cancel.after', $order);
|
$this->order = $cancelledOrder;
|
||||||
|
HookManager::call('order.cancel.after', $cancelledOrder);
|
||||||
return true;
|
return true;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
DB::rollBack();
|
|
||||||
Log::error($e);
|
Log::error($e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
"No active subscription. Unable to use our provided Apple ID": "No active subscription. Unable to use our provided Apple ID",
|
"No active subscription. Unable to use our provided Apple ID": "No active subscription. Unable to use our provided Apple ID",
|
||||||
"Oops, there's a problem... Please refresh the page and try again later": "Oops, there's a problem... Please refresh the page and try again later",
|
"Oops, there's a problem... Please refresh the page and try again later": "Oops, there's a problem... Please refresh the page and try again later",
|
||||||
"Order does not exist": "Order does not exist",
|
"Order does not exist": "Order does not exist",
|
||||||
|
"Order amount is invalid": "Order amount is invalid",
|
||||||
"Order does not exist or has been paid": "Order does not exist or has been paid",
|
"Order does not exist or has been paid": "Order does not exist or has been paid",
|
||||||
"Payment failed. Please check your credit card information": "Payment failed. Please check your credit card information",
|
"Payment failed. Please check your credit card information": "Payment failed. Please check your credit card information",
|
||||||
"Payment gateway request failed": "Payment gateway request failed",
|
"Payment gateway request failed": "Payment gateway request failed",
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
"No active subscription. Unable to use our provided Apple ID": "Нет активной подписки. Невозможно использовать предоставленный Apple ID",
|
"No active subscription. Unable to use our provided Apple ID": "Нет активной подписки. Невозможно использовать предоставленный Apple ID",
|
||||||
"Oops, there's a problem... Please refresh the page and try again later": "Ой, возникла проблема... Обновите страницу и попробуйте позже",
|
"Oops, there's a problem... Please refresh the page and try again later": "Ой, возникла проблема... Обновите страницу и попробуйте позже",
|
||||||
"Order does not exist": "Заказ не существует",
|
"Order does not exist": "Заказ не существует",
|
||||||
|
"Order amount is invalid": "Некорректная сумма заказа",
|
||||||
"Order does not exist or has been paid": "Заказ не существует или уже оплачен",
|
"Order does not exist or has been paid": "Заказ не существует или уже оплачен",
|
||||||
"Payment failed. Please check your credit card information": "Ошибка оплаты. Проверьте данные карты",
|
"Payment failed. Please check your credit card information": "Ошибка оплаты. Проверьте данные карты",
|
||||||
"Payment gateway request failed": "Ошибка запроса к платёжному шлюзу",
|
"Payment gateway request failed": "Ошибка запроса к платёжному шлюзу",
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
"Withdrawal account": "提现账号",
|
"Withdrawal account": "提现账号",
|
||||||
"Unsupported withdrawal": "不支持提现",
|
"Unsupported withdrawal": "不支持提现",
|
||||||
"Order does not exist": "订单不存在",
|
"Order does not exist": "订单不存在",
|
||||||
|
"Order amount is invalid": "订单金额异常",
|
||||||
"You have an unpaid or pending order, please try again later or cancel it": "您有未付款或开通中的订单,请稍后再试或将其取消",
|
"You have an unpaid or pending order, please try again later or cancel it": "您有未付款或开通中的订单,请稍后再试或将其取消",
|
||||||
"This subscription has been sold out, please choose another subscription": "该订阅已售罄,请更换其它订阅",
|
"This subscription has been sold out, please choose another subscription": "该订阅已售罄,请更换其它订阅",
|
||||||
"This subscription cannot be renewed, please change to another subscription": "该订阅无法续费,请更换其它订阅",
|
"This subscription cannot be renewed, please change to another subscription": "该订阅无法续费,请更换其它订阅",
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
"Withdrawal account": "提現賬號",
|
"Withdrawal account": "提現賬號",
|
||||||
"Unsupported withdrawal": "不支持提現",
|
"Unsupported withdrawal": "不支持提現",
|
||||||
"Order does not exist": "訂單不存在",
|
"Order does not exist": "訂單不存在",
|
||||||
|
"Order amount is invalid": "訂單金額異常",
|
||||||
"You have an unpaid or pending order, please try again later or cancel it": "您有未付款或開通中的訂單,請稍後再試或將其取消",
|
"You have an unpaid or pending order, please try again later or cancel it": "您有未付款或開通中的訂單,請稍後再試或將其取消",
|
||||||
"This subscription has been sold out, please choose another subscription": "該訂閱已售罄,請更換其它訂閱",
|
"This subscription has been sold out, please choose another subscription": "該訂閱已售罄,請更換其它訂閱",
|
||||||
"This subscription cannot be renewed, please change to another subscription": "該訂閱無法續費,請更換其它訂閱",
|
"This subscription cannot be renewed, please change to another subscription": "該訂閱無法續費,請更換其它訂閱",
|
||||||
|
|||||||
@ -6,7 +6,6 @@ use App\Models\Server;
|
|||||||
use App\Models\ServerMachine;
|
use App\Models\ServerMachine;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Bus;
|
use Illuminate\Support\Facades\Bus;
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class ServerHandshakeTest extends TestCase
|
class ServerHandshakeTest extends TestCase
|
||||||
@ -18,7 +17,7 @@ class ServerHandshakeTest extends TestCase
|
|||||||
parent::setUp();
|
parent::setUp();
|
||||||
|
|
||||||
config()->set('app.key', 'base64:' . base64_encode(str_repeat('a', 32)));
|
config()->set('app.key', 'base64:' . base64_encode(str_repeat('a', 32)));
|
||||||
Cache::forever('admin_settings', [
|
admin_setting([
|
||||||
'server_token' => 'server-token',
|
'server_token' => 'server-token',
|
||||||
'server_ws_enable' => 0,
|
'server_ws_enable' => 0,
|
||||||
]);
|
]);
|
||||||
|
|||||||
9
tests/TestCase.php
Normal file
9
tests/TestCase.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||||
|
|
||||||
|
abstract class TestCase extends BaseTestCase
|
||||||
|
{
|
||||||
|
}
|
||||||
193
tests/Unit/Services/OrderServiceConcurrencyTest.php
Normal file
193
tests/Unit/Services/OrderServiceConcurrencyTest.php
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Services;
|
||||||
|
|
||||||
|
use App\Exceptions\ApiException;
|
||||||
|
use App\Models\GiftCardCode;
|
||||||
|
use App\Models\GiftCardTemplate;
|
||||||
|
use App\Models\Order;
|
||||||
|
use App\Models\Plan;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\GiftCardService;
|
||||||
|
use App\Services\OrderService;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class OrderServiceConcurrencyTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_cancel_only_refunds_once_when_called_with_stale_order_models(): void
|
||||||
|
{
|
||||||
|
$user = $this->makeUser(['balance' => 0]);
|
||||||
|
$plan = $this->makePlan();
|
||||||
|
$order = $this->makeOrder($user, $plan, [
|
||||||
|
'status' => Order::STATUS_PENDING,
|
||||||
|
'balance_amount' => 100,
|
||||||
|
'total_amount' => 1000,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$firstStaleOrder = Order::findOrFail($order->id);
|
||||||
|
$secondStaleOrder = Order::findOrFail($order->id);
|
||||||
|
|
||||||
|
$this->assertTrue((new OrderService($firstStaleOrder))->cancel());
|
||||||
|
$this->assertFalse((new OrderService($secondStaleOrder))->cancel());
|
||||||
|
$this->assertSame(100, User::findOrFail($user->id)->balance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_open_only_applies_subscription_once_when_called_with_stale_order_models(): void
|
||||||
|
{
|
||||||
|
$user = $this->makeUser([
|
||||||
|
'balance' => 0,
|
||||||
|
'expired_at' => 0,
|
||||||
|
'transfer_enable' => 0,
|
||||||
|
'u' => 1073741824,
|
||||||
|
'd' => 0,
|
||||||
|
]);
|
||||||
|
$plan = $this->makePlan();
|
||||||
|
$order = $this->makeOrder($user, $plan, [
|
||||||
|
'status' => Order::STATUS_PROCESSING,
|
||||||
|
'balance_amount' => 1100,
|
||||||
|
'total_amount' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$before = time();
|
||||||
|
$firstStaleOrder = Order::findOrFail($order->id);
|
||||||
|
$secondStaleOrder = Order::findOrFail($order->id);
|
||||||
|
|
||||||
|
(new OrderService($firstStaleOrder))->open();
|
||||||
|
(new OrderService($secondStaleOrder))->open();
|
||||||
|
|
||||||
|
$user->refresh();
|
||||||
|
$this->assertSame(1, $user->reset_count);
|
||||||
|
$this->assertSame(Order::STATUS_COMPLETED, Order::findOrFail($order->id)->status);
|
||||||
|
$this->assertLessThan($before + (45 * 86400), $user->expired_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_gift_card_redeem_only_grants_rewards_once_for_stale_code_models(): void
|
||||||
|
{
|
||||||
|
$user = $this->makeUser(['balance' => 0]);
|
||||||
|
$template = GiftCardTemplate::create([
|
||||||
|
'name' => 'race-test',
|
||||||
|
'description' => 'race-test',
|
||||||
|
'type' => GiftCardTemplate::TYPE_GENERAL,
|
||||||
|
'status' => 1,
|
||||||
|
'rewards' => ['balance' => 100],
|
||||||
|
'admin_id' => 1,
|
||||||
|
'created_at' => time(),
|
||||||
|
'updated_at' => time(),
|
||||||
|
]);
|
||||||
|
GiftCardCode::create([
|
||||||
|
'template_id' => $template->id,
|
||||||
|
'code' => 'RACEUNITTEST',
|
||||||
|
'status' => GiftCardCode::STATUS_UNUSED,
|
||||||
|
'usage_count' => 0,
|
||||||
|
'max_usage' => 1,
|
||||||
|
'created_at' => time(),
|
||||||
|
'updated_at' => time(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$firstService = (new GiftCardService('RACEUNITTEST'))->setUser(User::findOrFail($user->id));
|
||||||
|
$secondService = (new GiftCardService('RACEUNITTEST'))->setUser(User::findOrFail($user->id));
|
||||||
|
|
||||||
|
$firstService->validate();
|
||||||
|
$secondService->validate();
|
||||||
|
$firstService->redeem();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$secondService->redeem();
|
||||||
|
$this->fail('The second stale gift card redeem should fail.');
|
||||||
|
} catch (ApiException $exception) {
|
||||||
|
$this->assertNotSame('', $exception->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->refresh();
|
||||||
|
$code = GiftCardCode::where('code', 'RACEUNITTEST')->firstOrFail();
|
||||||
|
$this->assertSame(100, $user->balance);
|
||||||
|
$this->assertSame(1, $code->usage_count);
|
||||||
|
$this->assertSame(1, $code->usages()->count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_checkout_rejects_negative_order_amount(): void
|
||||||
|
{
|
||||||
|
$user = $this->makeUser(['balance' => 0]);
|
||||||
|
Sanctum::actingAs($user);
|
||||||
|
|
||||||
|
$order = $this->makeOrder($user, $this->makePlan(), [
|
||||||
|
'status' => Order::STATUS_PENDING,
|
||||||
|
'total_amount' => -1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/v1/user/order/checkout', [
|
||||||
|
'trade_no' => $order->trade_no,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertStatus(400);
|
||||||
|
$this->assertSame(Order::STATUS_PENDING, Order::findOrFail($order->id)->status);
|
||||||
|
$this->assertNull(User::findOrFail($user->id)->plan_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeUser(array $overrides = []): User
|
||||||
|
{
|
||||||
|
return User::create(array_merge([
|
||||||
|
'email' => 'race-test@example.com',
|
||||||
|
'password' => 'password',
|
||||||
|
'uuid' => '00000000-0000-0000-0000-000000000001',
|
||||||
|
'token' => '0123456789abcdef0123456789abcdef',
|
||||||
|
'balance' => 0,
|
||||||
|
'commission_balance' => 0,
|
||||||
|
'transfer_enable' => 0,
|
||||||
|
'u' => 0,
|
||||||
|
'd' => 0,
|
||||||
|
'banned' => 0,
|
||||||
|
'is_admin' => 0,
|
||||||
|
'is_staff' => 0,
|
||||||
|
'expired_at' => 0,
|
||||||
|
'remind_expire' => 1,
|
||||||
|
'remind_traffic' => 1,
|
||||||
|
'created_at' => time(),
|
||||||
|
'updated_at' => time(),
|
||||||
|
], $overrides));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makePlan(array $overrides = []): Plan
|
||||||
|
{
|
||||||
|
return Plan::create(array_merge([
|
||||||
|
'group_id' => null,
|
||||||
|
'transfer_enable' => 1111,
|
||||||
|
'name' => 'Race Test Plan',
|
||||||
|
'speed_limit' => null,
|
||||||
|
'show' => 1,
|
||||||
|
'sort' => 0,
|
||||||
|
'renew' => 1,
|
||||||
|
'prices' => [
|
||||||
|
Plan::PERIOD_MONTHLY => 11,
|
||||||
|
],
|
||||||
|
'reset_traffic_method' => Plan::RESET_TRAFFIC_MONTHLY,
|
||||||
|
'capacity_limit' => null,
|
||||||
|
'sell' => 1,
|
||||||
|
'device_limit' => null,
|
||||||
|
'created_at' => time(),
|
||||||
|
'updated_at' => time(),
|
||||||
|
], $overrides));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function makeOrder(User $user, Plan $plan, array $overrides = []): Order
|
||||||
|
{
|
||||||
|
return Order::create(array_merge([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'plan_id' => $plan->id,
|
||||||
|
'type' => Order::TYPE_NEW_PURCHASE,
|
||||||
|
'period' => Plan::PERIOD_MONTHLY,
|
||||||
|
'trade_no' => uniqid('race_', true),
|
||||||
|
'total_amount' => 0,
|
||||||
|
'balance_amount' => 0,
|
||||||
|
'status' => Order::STATUS_PENDING,
|
||||||
|
'commission_status' => 0,
|
||||||
|
'commission_balance' => 0,
|
||||||
|
'created_at' => time(),
|
||||||
|
'updated_at' => time(),
|
||||||
|
], $overrides));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user