|
@@ -4,9 +4,13 @@ namespace Tests\Integration;
|
|
|
|
|
|
|
|
use App\Http\Controllers\admin\PaymentConfiguration;
|
|
use App\Http\Controllers\admin\PaymentConfiguration;
|
|
|
use App\Http\Controllers\admin\RechargeChannel;
|
|
use App\Http\Controllers\admin\RechargeChannel;
|
|
|
|
|
+use App\Http\Controllers\admin\PaymentCompany;
|
|
|
use App\Services\PaymentChannelLinkService;
|
|
use App\Services\PaymentChannelLinkService;
|
|
|
use App\Services\Payment\RechargeChannelConfigurationService;
|
|
use App\Services\Payment\RechargeChannelConfigurationService;
|
|
|
use App\Models\PaymentGateway;
|
|
use App\Models\PaymentGateway;
|
|
|
|
|
+use App\Models\RechargeChannel as ChannelModel;
|
|
|
|
|
+use App\Services\Payment\PaymentProviderService;
|
|
|
|
|
+use App\Services\PaymentOrderService;
|
|
|
use Illuminate\Config\Repository;
|
|
use Illuminate\Config\Repository;
|
|
|
use Illuminate\Database\Capsule\Manager;
|
|
use Illuminate\Database\Capsule\Manager;
|
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
@@ -60,6 +64,12 @@ class PaymentChannelOptionsTest extends TestCase
|
|
|
$fixture = require dirname(__DIR__) . '/fixtures/payment_provider_channels.php';
|
|
$fixture = require dirname(__DIR__) . '/fixtures/payment_provider_channels.php';
|
|
|
DB::table('recharge_channel')->insert($fixture['channels']);
|
|
DB::table('recharge_channel')->insert($fixture['channels']);
|
|
|
DB::table('recharge_channel_group')->insert($fixture['groups']);
|
|
DB::table('recharge_channel_group')->insert($fixture['groups']);
|
|
|
|
|
+ (require dirname(__DIR__, 2) . '/database/migrations/2026_09_08_160000_add_payment_provider_status_and_channel_soft_deletes.php')->up();
|
|
|
|
|
+ $db->schema()->create('operation_audits', function (Blueprint $t) {
|
|
|
|
|
+ $t->id(); $t->string('resource_type'); $t->unsignedBigInteger('resource_id')->nullable();
|
|
|
|
|
+ $t->string('action'); $t->json('changes'); $t->unsignedBigInteger('operator_id')->nullable();
|
|
|
|
|
+ $t->string('operator_name'); $t->timestamp('created_at')->useCurrent();
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
protected function tearDown(): void
|
|
protected function tearDown(): void
|
|
@@ -284,4 +294,206 @@ class PaymentChannelOptionsTest extends TestCase
|
|
|
$method->invoke($controller, PaymentGateway::class, ['kind' => 'deposit', 'recharge_channel_id' => 26,
|
|
$method->invoke($controller, PaymentGateway::class, ['kind' => 'deposit', 'recharge_channel_id' => 26,
|
|
|
'recharge_channel_group_ids' => [1], 'payment_company' => 'no', 'payment_method' => 'NOpay12'], 'payment_gateway');
|
|
'recharge_channel_group_ids' => [1], 'payment_company' => 'no', 'payment_method' => 'NOpay12'], 'payment_gateway');
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ private function setCompanyStatus(string $code, int $status): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->app->instance('request', Request::create('/admin/paymentCompany/status', 'POST', ['payment_company' => $code, 'status' => $status]));
|
|
|
|
|
+ return (new PaymentCompany())->status(new PaymentProviderService())->getData(true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function deleteChannel(int $id): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->app->instance('request', Request::create('/admin/rechargeChannel/delete', 'POST', ['id' => $id]));
|
|
|
|
|
+ return (new RechargeChannel())->delete(new RechargeChannelConfigurationService())->getData(true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_company_management_lists_registered_providers_and_filters_status(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->request('/admin/paymentCompany/list', []);
|
|
|
|
|
+ $result = (new PaymentCompany())->list(new PaymentProviderService())->getData(true);
|
|
|
|
|
+ $this->assertSame(0, $result['code']);
|
|
|
|
|
+ $this->assertSame(5, $result['data']['total']);
|
|
|
|
|
+ $rows = array_column($result['data']['data'], null, 'payment_company');
|
|
|
|
|
+ $this->assertSame('SHA256', $rows['no']['signature_algorithm']);
|
|
|
|
|
+ $this->assertSame([1, 2], $rows['no']['data_types']);
|
|
|
|
|
+ $this->assertSame(3, $rows['no']['channel_count']);
|
|
|
|
|
+ $this->assertSame(0, $this->setCompanyStatus('no', 0)['code']);
|
|
|
|
|
+ $this->assertSame(0, $this->setCompanyStatus('no', 0)['code']);
|
|
|
|
|
+ $this->assertSame(1, DB::table('operation_audits')->where('resource_type', 'payment_company')->count());
|
|
|
|
|
+ $this->request('/admin/paymentCompany/list', ['status' => 0]);
|
|
|
|
|
+ $this->assertSame(1, (new PaymentCompany())->list(new PaymentProviderService())->getData(true)['data']['total']);
|
|
|
|
|
+ $this->assertSame(-3, $this->setCompanyStatus('unknown', 0)['code']);
|
|
|
|
|
+ $this->assertSame(-3, $this->setCompanyStatus('no', 9)['code']);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_company_switch_disables_options_and_runtime_without_rewriting_channels(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $groups = DB::table('recharge_channel_group')->get()->toJson();
|
|
|
|
|
+ $this->assertSame(0, $this->setCompanyStatus('no', 0)['code']);
|
|
|
|
|
+ $this->assertFalse(ChannelModel::checkRechargeChannel('NOpay12', 1));
|
|
|
|
|
+ $this->assertFalse(ChannelModel::checkWithdrawChannel('NOwithdraw', 1));
|
|
|
|
|
+ $this->assertNotFalse(ChannelModel::checkRechargeChannel('JDpay', 1));
|
|
|
|
|
+ $this->assertNotFalse(ChannelModel::checkRechargeChannel('usdt', 1));
|
|
|
|
|
+ $this->assertArrayNotHasKey('NOpay12', ChannelModel::product(1));
|
|
|
|
|
+ $this->assertSame(1, DB::table('recharge_channel')->where('id', 26)->value('status'));
|
|
|
|
|
+ $this->assertSame($groups, DB::table('recharge_channel_group')->get()->toJson());
|
|
|
|
|
+ $this->request('/admin/paymentConfig/options', ['kind' => 'deposit']);
|
|
|
|
|
+ $options = (new PaymentConfiguration(new PaymentChannelLinkService()))->options()->getData(true)['data'];
|
|
|
|
|
+ $companies = array_column($options['payment_companies'], null, 'value');
|
|
|
|
|
+ $this->assertSame('provider_disabled', $companies['no']['disabled_reason_code']);
|
|
|
|
|
+ $this->assertFalse(array_column($options['recharge_channels'], null, 'id')[26]['selectable']);
|
|
|
|
|
+ $form = $this->formOptions();
|
|
|
|
|
+ $this->assertTrue(array_column($form['payment_companies'], null, 'value')['no']['disabled']);
|
|
|
|
|
+ $row = $this->channelList(['data_type' => '1', 'type' => 'NOpay12'])['data'][0];
|
|
|
|
|
+ $this->assertSame(1, $row['status']);
|
|
|
|
|
+ $this->assertSame(0, $row['company_status']);
|
|
|
|
|
+ $this->assertSame(0, $row['effective_status']);
|
|
|
|
|
+ $this->assertSame(-3, $this->saveChannel($this->newChannel())['code']);
|
|
|
|
|
+ $this->assertSame(0, $this->setCompanyStatus('no', 1)['code']);
|
|
|
|
|
+ $this->assertNotFalse(ChannelModel::checkRechargeChannel('NOpay12', 1));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_reenable_company_does_not_enable_individually_disabled_channels(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->setCompanyStatus('zimu', 0);
|
|
|
|
|
+ $this->setCompanyStatus('zimu', 1);
|
|
|
|
|
+ $this->assertSame(0, DB::table('recharge_channel')->where('id', 31)->value('status'));
|
|
|
|
|
+ $this->assertFalse(ChannelModel::checkWithdrawChannel('ZIMUcash', 1));
|
|
|
|
|
+ $this->assertNotFalse(ChannelModel::checkWithdrawChannel('ZIMUwithdraw', 1));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_disabled_company_blocks_new_external_requests_before_any_wallet_or_network_work(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->setCompanyStatus('no', 0);
|
|
|
|
|
+ $this->assertSame('支付公司已禁用', PaymentOrderService::createPay('test-user', 100, 'NOpay12')['text']);
|
|
|
|
|
+ $this->assertSame('支付公司已禁用', PaymentOrderService::autoCreatePayout('test-user', 100, 'NOwithdraw', '', '', '')['text']);
|
|
|
|
|
+ $this->assertFalse(PaymentProviderService::requestEnabled('nopay12', 1));
|
|
|
|
|
+ $this->assertTrue(PaymentProviderService::requestEnabled('JDpay', 1));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_delete_last_deposit_type_soft_deletes_and_cleans_only_its_group_membership(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $beforeWithdraw = DB::table('recharge_channel_group')->where('id', 1)->value('withdraw_type');
|
|
|
|
|
+ $this->assertSame(0, $this->deleteChannel(24)['code']);
|
|
|
|
|
+ $this->assertNull(ChannelModel::query()->find(24));
|
|
|
|
|
+ $deleted = ChannelModel::withTrashed()->findOrFail(24);
|
|
|
|
|
+ $this->assertTrue($deleted->trashed());
|
|
|
|
|
+ $this->assertSame(0, (int)$deleted->status);
|
|
|
|
|
+ $this->assertSame('JD钱包', $deleted->name);
|
|
|
|
|
+ $types = explode(',', DB::table('recharge_channel_group')->where('id', 1)->value('recharge_type'));
|
|
|
|
|
+ $this->assertNotContains('JDpay', $types);
|
|
|
|
|
+ $this->assertContains('NOpay12', $types);
|
|
|
|
|
+ $this->assertSame($beforeWithdraw, DB::table('recharge_channel_group')->where('id', 1)->value('withdraw_type'));
|
|
|
|
|
+ $this->assertNotFalse(ChannelModel::checkWithdrawChannel('JDpay', 1));
|
|
|
|
|
+ $this->assertFalse(ChannelModel::checkRechargeChannel('JDpay', 1));
|
|
|
|
|
+ $this->assertSame(0, $this->deleteChannel(24)['code']);
|
|
|
|
|
+ $this->assertSame(1, DB::table('operation_audits')->where('resource_type', 'recharge_channel')->count());
|
|
|
|
|
+ $this->setCompanyStatus('jd', 0); $this->setCompanyStatus('jd', 1);
|
|
|
|
|
+ $this->assertNull(ChannelModel::query()->find(24));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_delete_one_of_several_same_type_channels_preserves_groups_until_last_removed(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->assertSame(0, $this->deleteChannel(6)['code']);
|
|
|
|
|
+ $this->assertStringContainsString('zfbsm', DB::table('recharge_channel_group')->where('id', 1)->value('recharge_type'));
|
|
|
|
|
+ $this->assertSame(3, ChannelModel::query()->where('data_type', 1)->where('type', 'zfbsm')->count());
|
|
|
|
|
+ foreach ([7, 8, 9] as $id) $this->assertSame(0, $this->deleteChannel($id)['code']);
|
|
|
|
|
+ $this->assertStringNotContainsString('zfbsm', DB::table('recharge_channel_group')->where('id', 1)->value('recharge_type'));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_delete_is_blocked_by_payment_config_reference_and_does_not_touch_activity(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ DB::connection()->getSchemaBuilder()->create('payment_gateways', function (Blueprint $t) {
|
|
|
|
|
+ $t->id(); $t->unsignedBigInteger('recharge_channel_id'); $t->softDeletes();
|
|
|
|
|
+ });
|
|
|
|
|
+ DB::table('payment_gateways')->insert(['recharge_channel_id' => 24]);
|
|
|
|
|
+ $this->assertSame(-3, $this->deleteChannel(24)['code']);
|
|
|
|
|
+ $this->assertNotNull(ChannelModel::query()->find(24));
|
|
|
|
|
+ $this->assertSame(-3, $this->deleteChannel(21)['code']);
|
|
|
|
|
+ $this->assertNotNull(ChannelModel::query()->find(21));
|
|
|
|
|
+ DB::table('payment_gateways')->update(['deleted_at' => now()]);
|
|
|
|
|
+ $this->assertSame(0, $this->deleteChannel(24)['code']);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_migration_retry_keeps_disabled_company_and_deleted_channel(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->setCompanyStatus('no', 0);
|
|
|
|
|
+ $this->deleteChannel(24);
|
|
|
|
|
+ (require dirname(__DIR__, 2) . '/database/migrations/2026_09_08_160000_add_payment_provider_status_and_channel_soft_deletes.php')->up();
|
|
|
|
|
+ $this->assertSame(0, PaymentProviderService::statuses()['no']);
|
|
|
|
|
+ $this->assertTrue(ChannelModel::withTrashed()->findOrFail(24)->trashed());
|
|
|
|
|
+ $this->assertSame(5, DB::table('payment_providers')->count());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private function paymentOrderFixture(int $type, string $channel, int $status): \App\Models\PaymentOrder
|
|
|
|
|
+ {
|
|
|
|
|
+ DB::connection()->getSchemaBuilder()->create('payment_orders', function (Blueprint $t) {
|
|
|
|
|
+ $t->id(); $t->integer('type'); $t->string('channel'); $t->integer('status');
|
|
|
|
|
+ $t->string('order_no'); $t->string('member_id'); $t->decimal('amount', 18, 4);
|
|
|
|
|
+ $t->string('state')->nullable(); $t->text('callback_data')->nullable(); $t->timestamps();
|
|
|
|
|
+ });
|
|
|
|
|
+ $this->app->instance('log', new class {
|
|
|
|
|
+ public function channel($channel) { return $this; }
|
|
|
|
|
+ public function error($message, array $context = []) {}
|
|
|
|
|
+ });
|
|
|
|
|
+ return \App\Models\PaymentOrder::query()->create(['type' => $type, 'channel' => $channel,
|
|
|
|
|
+ 'status' => $status, 'order_no' => 'test-order', 'member_id' => 'test-user', 'amount' => 10]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_pending_payout_cannot_be_sent_after_company_disabled(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $order = $this->paymentOrderFixture(2, 'NOwithdraw', PaymentOrderService::STATUS_STAY);
|
|
|
|
|
+ $this->setCompanyStatus('no', 0);
|
|
|
|
|
+ $result = PaymentOrderService::createPayout($order->id);
|
|
|
|
|
+ $this->assertSame(-3, $result['code']);
|
|
|
|
|
+ $this->assertSame('支付公司已禁用', $result['msg']);
|
|
|
|
|
+ $this->assertSame(PaymentOrderService::STATUS_STAY, $order->fresh()->status);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_pending_payout_cannot_be_sent_after_last_channel_deleted(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $order = $this->paymentOrderFixture(2, 'JDpay', PaymentOrderService::STATUS_STAY);
|
|
|
|
|
+ $this->assertSame(0, $this->deleteChannel(25)['code']);
|
|
|
|
|
+ $result = PaymentOrderService::createPayout($order->id);
|
|
|
|
|
+ $this->assertSame(-3, $result['code']);
|
|
|
|
|
+ $this->assertSame('提现通道已停用或删除', $result['msg']);
|
|
|
|
|
+ $this->assertSame(PaymentOrderService::STATUS_STAY, $order->fresh()->status);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_existing_pay_callback_is_not_blocked_by_company_switch(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $order = $this->paymentOrderFixture(1, 'NOpay12', PaymentOrderService::STATUS_PROCESS);
|
|
|
|
|
+ $this->setCompanyStatus('no', 0);
|
|
|
|
|
+ $method = new \ReflectionMethod(PaymentOrderService::class, 'applyPayCallback');
|
|
|
|
|
+ $method->setAccessible(true);
|
|
|
|
|
+ $this->assertTrue($method->invoke(null, $order, '10', 'failed', 'success', 'failed', ['status' => 'failed']));
|
|
|
|
|
+ $this->assertSame(PaymentOrderService::STATUS_FAIL, $order->fresh()->status);
|
|
|
|
|
+ $this->assertNotNull($order->fresh()->callback_data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_company_disabled_updates_saved_gateway_effective_status(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $channel = ChannelModel::query()->findOrFail(26);
|
|
|
|
|
+ $gateway = new PaymentGateway(['payment_company' => 'no', 'payment_method' => 'NOpay12',
|
|
|
|
|
+ 'status' => 1, 'recharge_channel_group_ids' => [1]]);
|
|
|
|
|
+ $gateway->setRelation('rechargeChannel', $channel);
|
|
|
|
|
+ $this->setCompanyStatus('no', 0);
|
|
|
|
|
+ $data = (new PaymentChannelLinkService())->formatOne($gateway);
|
|
|
|
|
+ $this->assertSame(1, $data['config_status']);
|
|
|
|
|
+ $this->assertSame(1, $data['channel_status']);
|
|
|
|
|
+ $this->assertSame(0, $data['company_status']);
|
|
|
|
|
+ $this->assertSame(0, $data['effective_status']);
|
|
|
|
|
+ $this->expectException(\RuntimeException::class);
|
|
|
|
|
+ $this->expectExceptionMessage('NO支付已禁用');
|
|
|
|
|
+ (new PaymentChannelLinkService())->validate(26, [1], 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public function test_empty_group_types_can_be_saved_after_last_channel_removed(): void
|
|
|
|
|
+ {
|
|
|
|
|
+ $this->app->instance('request', Request::create('/admin/rechargeChannel/updateGroup', 'POST',
|
|
|
|
|
+ ['id' => 1, 'name' => '全部充值', 'recharge_type' => [], 'withdraw_type' => []]));
|
|
|
|
|
+ $this->assertSame(0, (new RechargeChannel())->updateGroup()->getData(true)['code']);
|
|
|
|
|
+ $this->assertSame('', DB::table('recharge_channel_group')->where('id', 1)->value('recharge_type'));
|
|
|
|
|
+ $this->assertSame('yuebao,old_user,recharge', DB::table('recharge_channel_group')->where('id', 1)->value('activity_type'));
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|