'azerothcore_auth', 'database.connections.azerothcore_auth' => [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', 'foreign_key_constraints' => true, ], 'services.robokassa.mode' => 'test', 'services.robokassa.environments.test' => [ 'login' => 'moonwell-test', 'password1' => 'password-one', 'password2' => 'password-two', 'hash_algorithm' => 'md5', ], 'services.robokassa.environments.production' => [ 'login' => 'moonwell-production', 'password1' => 'production-one', 'password2' => 'production-two', 'hash_algorithm' => 'sha256', ], 'services.robokassa.tears_per_ruble' => 10, ]); DB::purge('azerothcore_auth'); $migration = require database_path('migrations/2026_07_19_000001_create_moonwell_balance_tables.php'); $migration->up(); } public function test_valid_callback_credits_balance_only_once(): void { $robokassa = app(RobokassaService::class); $invoice = $robokassa->createInvoice(7, '100.00'); $parameters = $robokassa->paymentParameters($invoice, 'player@example.com'); $callback = [ 'OutSum' => '100.00', 'InvId' => (string) $invoice->id, 'Shp_account' => '7', 'SignatureValue' => md5('100.00:'.$invoice->id.':password-two:Shp_account=7'), ]; $robokassa->processResult($callback); $robokassa->processResult($callback); $this->assertSame('1', $parameters['IsTest']); $this->assertSame('1000.00', app(BalanceService::class)->balance(7)); $this->assertDatabaseCount('balance_transactions', 1, 'azerothcore_auth'); } public function test_game_spending_is_atomic_and_idempotent(): void { DB::connection('azerothcore_auth')->table('account_balances')->insert([ 'account_id' => 7, 'balance' => '100.00', 'created_at' => now(), 'updated_at' => now(), ]); $balances = app(BalanceService::class); $first = $balances->spend(7, '25.50', 'game:purchase-42', 'Игровой предмет'); $second = $balances->spend(7, '25.50', 'game:purchase-42', 'Игровой предмет'); $this->assertSame($first->id, $second->id); $this->assertSame('74.50', $balances->balance(7)); $this->assertDatabaseCount('balance_transactions', 1, 'azerothcore_auth'); } public function test_production_mode_uses_production_credentials_and_disables_test_flag(): void { config(['services.robokassa.mode' => 'production']); $robokassa = app(RobokassaService::class); $invoice = $robokassa->createInvoice(7, '50.00'); $parameters = $robokassa->paymentParameters($invoice); $this->assertSame('production', $invoice->mode); $this->assertSame('moonwell-production', $parameters['MerchantLogin']); $this->assertSame('0', $parameters['IsTest']); $this->assertSame( hash('sha256', 'moonwell-production:50.00:'.$invoice->id.':production-one:Shp_account=7'), $parameters['SignatureValue'], ); } public function test_admin_can_credit_and_debit_with_audit_metadata(): void { $balances = app(BalanceService::class); $credit = $balances->adjustByAdmin(7, '50.00', 'credit', 1, 'ADMIN', 'Компенсация'); $debit = $balances->adjustByAdmin(7, '12.50', 'debit', 1, 'ADMIN', 'Исправление'); $this->assertSame('37.50', $balances->balance(7)); $this->assertSame('admin_credit', $credit->type); $this->assertSame('admin_debit', $debit->type); $this->assertSame('ADMIN', $debit->metadata['admin_username']); $this->assertSame('Исправление', $debit->metadata['reason']); } public function test_admin_cannot_debit_more_than_available_balance(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Недостаточно средств'); app(BalanceService::class)->adjustByAdmin(7, '1.00', 'debit', 1, 'ADMIN', 'Проверка'); } }