CaiWuXiTong/tests/Feature/Business/MarketingAccountBillsTabsTest.php
WuYongJie 43445fde4b feat: read marketing bills from business mirror
首次全量完成后,营销账户账单、余额、汇总、选项及分页优先读取 caiwu_business_mirror;未就绪时保留旧快照链路。
2026-08-14 17:48:23 +08:00

393 lines
16 KiB
PHP

<?php
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use Mockery;
use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException;
use Modules\EcommerceFinance\Repositories\JdWalletBillRepository;
use Modules\EcommerceFinance\Repositories\MarketingAccountBillMirrorRepository;
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository;
use Modules\EcommerceFinance\Services\UpstreamQueryCache;
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
use Tests\Feature\FeatureTestCase;
class MarketingAccountBillsTabsTest extends FeatureTestCase
{
protected function setUp(): void
{
parent::setUp();
$mirror = Mockery::mock(MarketingAccountBillMirrorRepository::class);
$mirror->shouldReceive('ready')->byDefault()->andReturnFalse();
$this->app->instance(MarketingAccountBillMirrorRepository::class, $mirror);
}
public function testMarketingAccountBalanceRedirectsToTaobaoAndShowsRestoredControls(): void
{
$this->allowMarketingAccountBalancePage();
$repository = Mockery::mock(PinduoduoSpreadBillRepository::class);
$repository->shouldReceive('usesBusinessMirror')->once()->andReturnFalse();
$this->app->instance(PinduoduoSpreadBillRepository::class, $repository);
$cache = Mockery::mock(UpstreamQueryCache::class);
$cache->shouldReceive('remember')->twice()->andReturnUsing(
fn (string $scope): array => str_ends_with($scope, '.options') ? [] : ['rows' => [], 'total' => 0]
);
$factory = Mockery::mock(UpstreamQueryCacheFactory::class);
$factory->shouldReceive('forSource')->once()->with('jiansun')->andReturn($cache);
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'marketing-account-balance',
]))
->assertRedirect(route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'marketing-account-balance',
'tab' => 'taobao-alipay',
]));
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'marketing-account-balance',
'tab' => 'pinduoduo',
]))
->assertOk()
->assertSeeText('淘系账户')
->assertSeeText('拼多多账户')
->assertSeeText('店铺名称')
->assertSeeText('期初余额')
->assertSeeText('系统期末余额')
->assertSeeText('查询')
->assertSeeText('清除')
->assertSeeText('导出')
->assertDontSeeText('该页面暂未配置数据表');
}
public function testMarketingAccountBillsOnlyRenamesTheRequestedTabs(): void
{
$this->allowMarketingAccountBillsPage();
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'marketing-account-bills',
'tab' => 'taobao-wechat',
]))
->assertOk()
->assertSeeText('淘系支付宝账户')
->assertSeeText('Alibaba')
->assertSeeText('京东')
->assertDontSeeText('淘系微信账户')
->assertDontSeeText('京东网银钱包账户')
->assertSee('class="my-6 business-page--marketing-account-bills"', false);
}
public function testJdWalletHasRechargeAndConsumptionViewsWithoutChangingTheDataSource(): void
{
$this->allowMarketingAccountBillsPage();
$base = [
'section' => 'billing-transactions',
'page' => 'marketing-account-bills',
'tab' => 'jd-wallet',
];
$this->loginAs()
->get(route('business.pages.show', $base))
->assertOk()
->assertSeeText('充值账单')
->assertSeeText('消耗账单')
->assertSeeText('金额(元)')
->assertSeeText('充值时间')
->assertDontSeeText('投放日期')
->assertSee('data-jd-wallet-panel', false)
->assertSee('data-jd-wallet-navigation', false);
$this->loginAs()
->get(route('business.pages.show', array_merge($base, ['jd_ledger' => 'consumption'])))
->assertOk()
->assertSeeText('充值账单')
->assertSeeText('消耗账单')
->assertSeeText('金额(元)')
->assertSeeText('投放日期')
->assertSeeText('结算日期')
->assertDontSeeText('充值时间');
}
public function testJdWalletColdStartRegistersEveryRequiredSnapshotScope(): void
{
$this->allowMarketingAccountBillsPage();
$scopes = [];
$cache = Mockery::mock(UpstreamQueryCache::class);
$cache->shouldReceive('remember')
->times(3)
->andReturnUsing(function (string $scope, array $parameters, callable $resolver) use (&$scopes): array {
$scopes[] = $scope;
if (count($scopes) === 1) {
throw new ServiceUnavailableHttpException(30, '首份共享快照正在准备。');
}
return match ($scope) {
'jd-wallet-bills.recharge.summary' => [
'amount_total' => 0,
'balance_total' => 0,
'count' => 0,
],
'jd-wallet-bills.options' => [],
default => ['rows' => [], 'total' => 0],
};
});
$factory = Mockery::mock(UpstreamQueryCacheFactory::class);
$factory->shouldReceive('forSource')
->once()
->with('jiansun')
->andReturn($cache);
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
$this->app->instance(JdWalletBillRepository::class, Mockery::mock(JdWalletBillRepository::class));
$this->app->make(JdWalletBillRepository::class)
->shouldReceive('usesBusinessMirror')->once()->with('recharge')->andReturnFalse();
$this->app->make(JdWalletBillRepository::class)
->shouldReceive('usesBusinessMirrorForOptions')->once()->andReturnFalse();
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'marketing-account-bills',
'tab' => 'jd-wallet',
]))
->assertOk()
->assertSeeText('首份共享快照正在准备。');
$this->assertSame([
'jd-wallet-bills.recharge.list',
'jd-wallet-bills.recharge.summary',
'jd-wallet-bills.options',
], $scopes);
}
public function testMarketingAccountBalanceExportColdStartReturnsActionableRedirect(): void
{
$this->allowMarketingAccountBalancePage();
$cache = Mockery::mock(UpstreamQueryCache::class);
$cache->shouldReceive('remember')
->once()
->withArgs(function (string $scope, array $parameters, callable $resolver): bool {
return $scope === 'taobao-alipay-bills.list'
&& ($parameters['export'] ?? false) === true;
})
->andThrow(new ServiceUnavailableHttpException(30, '首份共享快照正在准备。'));
$factory = Mockery::mock(UpstreamQueryCacheFactory::class);
$factory->shouldReceive('forSource')
->once()
->with('jiansun')
->andReturn($cache);
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
$repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class);
$repository->shouldReceive('usesBusinessMirror')->once()->andReturnFalse();
$this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository);
$this->loginAs()
->get(route('business.pages.export', [
'section' => 'billing-transactions',
'page' => 'marketing-account-balance',
'tab' => 'taobao-alipay',
'store_name' => '测试店铺',
'month' => '2026-08',
]))
->assertRedirect(route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'marketing-account-balance',
'tab' => 'taobao-alipay',
'store_name' => '测试店铺',
'month' => '2026-08',
]))
->assertSessionHas('error', '首份共享快照正在准备,系统已登记刷新请求;请稍后刷新页面后再导出。');
}
public function testMarketingAccountBalanceColdStartShowsProcessingStateAndAutomaticCheck(): void
{
$this->allowMarketingAccountBalancePage();
$cache = Mockery::mock(UpstreamQueryCache::class);
$cache->shouldReceive('remember')
->once()
->withArgs(fn (string $scope): bool => $scope === 'taobao-alipay-bills.list')
->andThrow(new UpstreamSnapshotPendingException());
$cache->shouldReceive('remember')
->once()
->withArgs(fn (string $scope): bool => $scope === 'taobao-alipay-bills.options')
->andReturn([]);
$factory = Mockery::mock(UpstreamQueryCacheFactory::class);
$factory->shouldReceive('forSource')->once()->with('jiansun')->andReturn($cache);
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
$repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class);
$repository->shouldReceive('usesBusinessMirror')->once()->andReturnFalse();
$this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'marketing-account-balance',
'tab' => 'taobao-alipay',
]))
->assertOk()
->assertSeeText('同步请求已进入后台队列,页面会自动检查首份共享数据,无需重复操作。')
->assertSee('data-marketing-balance-pending', false)
->assertSee('window.location.reload()', false)
->assertDontSee('<div class="biz-alert biz-alert-error" role="alert">', false);
}
public function testMarketingAccountBalanceExportUnexpectedFailureReturnsRedactedRedirect(): void
{
$this->allowMarketingAccountBalancePage();
$cache = Mockery::mock(UpstreamQueryCache::class);
$cache->shouldReceive('remember')->once()->andThrow(new \RuntimeException('sensitive DSN details'));
$factory = Mockery::mock(UpstreamQueryCacheFactory::class);
$factory->shouldReceive('forSource')->once()->with('jiansun')->andReturn($cache);
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
$repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class);
$repository->shouldReceive('usesBusinessMirror')->once()->andReturnFalse();
$this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository);
$this->loginAs()
->get(route('business.pages.export', [
'section' => 'billing-transactions',
'page' => 'marketing-account-balance',
'tab' => 'taobao-alipay',
]))
->assertRedirect(route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'marketing-account-balance',
'tab' => 'taobao-alipay',
]))
->assertSessionHas('error', '余额数据导出暂时无法完成,请稍后重试。');
}
public function testMarketingBillSecondPageReadsReadyMirrorWithoutRegisteringAColdSnapshot(): void
{
$this->allowMarketingAccountBillsPage();
$repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class);
$repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue();
$repository->shouldReceive('paginate')
->once()
->with(null, null, null, null, 2, 25)
->andReturn(['rows' => [['store_name' => '镜像店铺']], 'total' => 26]);
$repository->shouldReceive('summary')->once()->andReturn([
'income_total' => 0,
'expense_total' => 0,
'balance' => 0,
'count' => 26,
]);
$repository->shouldReceive('storeOptions')->once()->andReturn(['镜像店铺']);
$this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository);
$cache = Mockery::mock(UpstreamQueryCache::class);
$cache->shouldNotReceive('remember');
$factory = Mockery::mock(UpstreamQueryCacheFactory::class);
$factory->shouldReceive('forSource')->once()->with('jiansun')->andReturn($cache);
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
$this->loginAs();
$url = route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'marketing-account-bills',
'tab' => 'taobao-alipay',
]) . '?page=2';
$this->get($url)
->assertOk()
->assertSeeText('镜像店铺')
->assertDontSeeText('首份共享快照正在准备');
}
public function testMarketingBalanceSecondPageReadsReadyMirrorWithoutRegisteringAColdSnapshot(): void
{
$this->allowMarketingAccountBalancePage();
$repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class);
$repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue();
$repository->shouldReceive('paginateMonthlyBalances')
->once()
->with(null, null, 2, 25)
->andReturn(['rows' => [[
'date' => '2026-08',
'store_name' => '镜像余额店铺',
'opening_balance' => 100,
'income_total' => 20,
'expense_total' => 5,
'closing_balance' => 115,
'system_closing_balance' => 115,
'balance_difference' => 0,
'difference_reason' => '',
'verification_status' => '一致',
'notes' => '',
]], 'total' => 26]);
$repository->shouldReceive('storeOptions')->once()->andReturn(['镜像余额店铺']);
$this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository);
$cache = Mockery::mock(UpstreamQueryCache::class);
$cache->shouldNotReceive('remember');
$factory = Mockery::mock(UpstreamQueryCacheFactory::class);
$factory->shouldReceive('forSource')->once()->with('jiansun')->andReturn($cache);
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
$this->loginAs();
$url = route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'marketing-account-balance',
'tab' => 'taobao-alipay',
]) . '?page=2';
$this->get($url)
->assertOk()
->assertSeeText('镜像余额店铺')
->assertDontSeeText('后台正在准备首份共享数据');
}
private function allowMarketingAccountBillsPage(): void
{
$this->user->update(['locale' => 'zh-CN']);
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$subject = 'user_' . $this->user->id;
setting()->set('system.sidebar_permissions', json_encode([
'configured' => [$subject],
'subjects' => [
$subject => ['billing-transactions/marketing-account-bills'],
],
], JSON_UNESCAPED_UNICODE));
}
private function allowMarketingAccountBalancePage(): void
{
$this->user->update(['locale' => 'zh-CN']);
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$subject = 'user_' . $this->user->id;
setting()->set('system.sidebar_permissions', json_encode([
'configured' => [$subject],
'subjects' => [
$subject => ['billing-transactions/marketing-account-balance'],
],
], JSON_UNESCAPED_UNICODE));
}
}