| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- <?php
- namespace Tests\Integration;
- use App\Http\Controllers\admin\JisuGameOrder;
- use App\Http\Controllers\admin\LhcOrder;
- use App\Http\Controllers\admin\Order;
- use App\Http\Controllers\admin\SportGameOrder;
- use Illuminate\Config\Repository;
- use Illuminate\Database\Capsule\Manager;
- use Illuminate\Database\Eloquent\Model;
- use Illuminate\Database\Schema\Blueprint;
- use Illuminate\Foundation\Application;
- use Illuminate\Http\Request;
- use Illuminate\Support\Facades\DB;
- use Illuminate\Support\Facades\Facade;
- use Illuminate\Translation\ArrayLoader;
- use Illuminate\Translation\Translator;
- use Illuminate\Validation\Factory;
- use PHPUnit\Framework\Attributes\DataProvider;
- use PHPUnit\Framework\TestCase;
- class AdminOrderRefundTest extends TestCase
- {
- private Application $app;
- private const CONTROLLERS = ['sport' => Order::class, 'game' => SportGameOrder::class, 'lottery' => LhcOrder::class, 'jisu' => JisuGameOrder::class];
- private const TABLES = ['sport' => 'sport_order', 'game' => 'sport_game_order', 'lottery' => 'lhc_order', 'jisu' => 'jisu_game_order'];
- protected function setUp(): void
- {
- Facade::clearResolvedInstances(); Model::clearBootedModels(); Model::unsetEventDispatcher();
- $this->app = new Application(dirname(__DIR__, 2));
- $this->app->instance('config', new Repository(['app' => ['locale' => 'zh']]));
- $this->app->instance('log', new \Psr\Log\NullLogger());
- $translator = new Translator(new ArrayLoader(), 'zh');
- $this->app->instance('translator', $translator);
- $this->app->instance('validator', new Factory($translator, $this->app));
- $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory());
- Request::macro('validate', function (array $rules) { return app('validator')->make($this->all(), $rules)->validate(); });
- Facade::setFacadeApplication($this->app);
- $db = new Manager($this->app);
- $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
- $db->setAsGlobal(); $db->bootEloquent();
- $this->app->instance('db', $db->getDatabaseManager()); $this->app->bind('db.schema', fn () => $db->schema());
- $db->schema()->create('wallets', function (Blueprint $t) { $t->id(); $t->string('member_id'); $t->decimal('available_balance', 24, 2); $t->timestamps(); });
- $db->schema()->create('balance_logs', function (Blueprint $t) {
- $t->id(); $t->string('member_id'); $t->decimal('amount', 24, 2); $t->decimal('before_balance', 24, 2); $t->decimal('after_balance', 24, 2);
- $t->string('change_type'); $t->string('remark'); $t->integer('related_id')->nullable(); $t->timestamps();
- });
- foreach (self::TABLES as $kind => $table) $db->schema()->create($table, function (Blueprint $t) use ($kind) {
- $t->id(); $t->string($kind === 'sport' ? 'user_id' : 'member_id'); $t->decimal('amount', 24, 2);
- if ($kind === 'lottery') { $t->string('ordernum'); $t->decimal('total_amount', 24, 2)->nullable(); $t->integer('lottery_status')->default(0); }
- else $t->integer('status')->default($kind === 'sport' ? 1 : 0);
- $t->integer('type')->default(1);
- if ($kind === 'sport') { $t->integer('pay_status')->default(1); $t->integer('return_status')->default(0); $t->integer('settlement_status')->default(0); $t->integer('return_operation_time')->nullable(); $t->integer('create_time')->nullable(); $t->integer('update_time')->nullable(); }
- else $t->timestamps();
- });
- $db->schema()->create('lhc_order_item', function (Blueprint $t) { $t->id(); $t->string('ordernum'); $t->timestamps(); });
- (require dirname(__DIR__, 2) . '/database/migrations/2026_11_03_120000_add_promotion_coupons.php')->up();
- DB::table('wallets')->insert(['member_id' => '1001', 'available_balance' => '50']);
- }
- protected function tearDown(): void
- {
- DB::disconnect(); Model::clearBootedModels(); Facade::clearResolvedInstances(); parent::tearDown();
- }
- public static function kinds(): array { return array_map(fn ($kind) => [$kind], array_keys(self::TABLES)); }
- private function seed(string $kind, int $id = 1, bool $coupon = true, bool $missingTotal = false): string
- {
- $coupon = $coupon && $kind !== 'jisu';
- $row = ['id' => $id, $kind === 'sport' ? 'user_id' : 'member_id' => '1001', 'amount' => '100'];
- if ($kind === 'lottery') {
- $row += ['ordernum' => 'slip-' . $id, 'total_amount' => $missingTotal ? null : '300'];
- foreach ([1, 2, 3] as $number) DB::table('lhc_order_item')->insert(['ordernum' => $row['ordernum']]);
- }
- if ($coupon) $row += ['coupon_id' => $id, 'coupon_discount' => '50'];
- DB::table(self::TABLES[$kind])->insert($row);
- if ($coupon) {
- DB::table('promotion_coupons')->insert(['id' => $id, 'activity_id' => 1, 'reward_id' => $id, 'user_id' => 1, 'member_id' => '1001',
- 'role' => 'inviter', 'inviter_user_id' => 1, 'seq' => 1, 'min_amount' => '100', 'discount' => '50', 'categories' => '["self:sport","self:lottery"]',
- 'valid_from' => '2000-01-01 00:00:00', 'valid_until' => '2099-12-31 23:59:59', 'status' => 'used']);
- DB::table('promotion_coupon_uses')->insert(['coupon_id' => $id, 'source' => self::TABLES[$kind], 'order_id' => $id, 'discount' => '50']);
- }
- return $kind === 'lottery' ? ($coupon ? '250.00' : '300.00') : ($coupon ? '50.00' : '100.00');
- }
- private function refund(string $kind, array $ids = [1]): array
- {
- $this->app->instance('request', Request::create('/admin/refund', 'POST', ['id' => $ids]));
- return (new (self::CONTROLLERS[$kind])())->refund()->getData(true);
- }
- private function snapshot(): array
- {
- $result = [];
- foreach (['wallets', 'balance_logs', 'promotion_coupons', 'promotion_coupon_uses', ...array_values(self::TABLES)] as $table) $result[$table] = DB::table($table)->orderBy('id')->get()->toJson();
- return $result;
- }
- #[DataProvider('kinds')]
- public function test_refund_credits_paid_amount_returns_coupon_and_retries_without_duplicates(string $kind): void
- {
- $refund = $this->seed($kind);
- $this->assertSame(0, $this->refund($kind)['code']);
- $this->assertSame(bcadd('50', $refund, 2), bcadd((string)DB::table('wallets')->value('available_balance'), '0', 2));
- $log = DB::table('balance_logs')->first();
- $this->assertSame($refund, bcadd((string)$log->amount, '0', 2));
- $this->assertSame(1, (int)$log->related_id);
- $this->assertSame('50.00', bcadd((string)$log->before_balance, '0', 2));
- $this->assertSame(bcadd('50', $refund, 2), bcadd((string)$log->after_balance, '0', 2));
- if ($kind !== 'jisu') $this->assertSame('unused', DB::table('promotion_coupons')->value('status'));
- $before = $this->snapshot();
- $this->assertSame(0, $this->refund($kind)['code']);
- $this->assertSame($before, $this->snapshot());
- }
- #[DataProvider('kinds')]
- public function test_refund_without_coupon_credits_the_complete_order(string $kind): void
- {
- $refund = $this->seed($kind, coupon: false);
- $this->assertSame(0, $this->refund($kind)['code']);
- $this->assertSame(bcadd('50', $refund, 2), bcadd((string)DB::table('wallets')->value('available_balance'), '0', 2));
- }
- #[DataProvider('kinds')]
- public function test_missing_wallet_rolls_back_terminal_state_and_coupon(string $kind): void
- {
- $this->seed($kind); DB::table('wallets')->delete(); $before = $this->snapshot();
- $this->assertSame(-3, $this->refund($kind)['code']);
- $this->assertSame($before, $this->snapshot());
- }
- #[DataProvider('kinds')]
- public function test_later_ledger_failure_rolls_back_the_entire_refund_batch(string $kind): void
- {
- $this->seed($kind); $this->seed($kind, 2); $before = $this->snapshot();
- DB::statement("CREATE TRIGGER reject_admin_refund BEFORE INSERT ON bot_balance_logs WHEN NEW.related_id = 2 BEGIN SELECT RAISE(ABORT, 'fixture ledger failure'); END");
- $this->assertSame(-3, $this->refund($kind, [1, 2])['code']);
- $this->assertSame($before, $this->snapshot());
- }
- public function test_legacy_lottery_missing_total_is_derived_from_its_numbers(): void
- {
- $this->seed('lottery', coupon: false, missingTotal: true);
- $this->assertSame(0, $this->refund('lottery')['code']);
- $this->assertSame('350.00', bcadd((string)DB::table('wallets')->value('available_balance'), '0', 2));
- }
- }
|