shouldReceive('ready')->byDefault()->andReturnFalse(); $this->app->instance(MarketingAccountBillMirrorRepository::class, $mirror); } public function testBillingNavigationSeparatesFlowAndBalancePages(): void { $navigation = collect(config('business_navigation')); $flowPages = collect($navigation->firstWhere('slug', 'billing-transactions')['pages']) ->pluck('slug')->all(); $balancePages = collect($navigation->firstWhere('slug', 'account-balance-data')['pages']) ->pluck('slug')->all(); $this->assertSame([ 'bank-transactions', 'store-receiving-bills', 'marketing-account-bills', 'deposit-account-bills', ], $flowPages); // 种菜明细表 / 聚水潭订单标签明细已移到并列的「种菜数据汇总」下 $plantingPages = collect($navigation->firstWhere('slug', 'planting-data')['pages']) ->pluck('slug')->all(); $this->assertSame(['planting-details', 'jst-order-labels'], $plantingPages); $this->assertNotContains('planting-details', $flowPages); $this->assertNotContains('jst-order-labels', $flowPages); $this->assertSame([ 'bank-balance', 'store-receiving-balance', 'marketing-account-balance', 'deposit-account-balance', ], $balancePages); } public function testMarketingAccountBillsOmitsRemovedPlatformTabs(): void { $page = collect(config('business_navigation')) ->firstWhere('slug', 'billing-transactions')['pages']; $tabs = collect(collect($page)->firstWhere('slug', 'marketing-account-bills')['tabs']) ->pluck('slug')->all(); $this->assertNotContains('douyin', $tabs); $this->assertNotContains('dewu', $tabs); $this->assertNotContains('vipshop', $tabs); $this->assertContains('jd-promotion', $tabs); $this->assertContains('douyin-promotion', $tabs); $this->assertContains('dewu-promotion', $tabs); $this->assertContains('vip-promotion', $tabs); } public function testLegacyMarketingAccountBalanceUrlRedirectsToBalanceMenu(): void { $this->allowMarketingAccountBalancePage(); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'billing-transactions', 'page' => 'marketing-account-balance', 'tab' => 'taobao-alipay', ])) ->assertRedirect(route('business.pages.show', [ 'section' => 'account-balance-data', 'page' => 'marketing-account-balance', 'tab' => 'taobao-alipay', ])); } 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' => 'account-balance-data', 'page' => 'marketing-account-balance', ])) ->assertRedirect(route('business.pages.show', [ 'section' => 'account-balance-data', 'page' => 'marketing-account-balance', 'tab' => 'taobao-alipay', ])); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'account-balance-data', 'page' => 'marketing-account-balance', 'tab' => 'pinduoduo', ])) ->assertOk() ->assertSeeText('淘系账户') ->assertSeeText('拼多多账户') ->assertSeeText('店铺名称') ->assertSeeText('期初余额') ->assertSeeText('系统期末余额') ->assertSeeText('查询') ->assertSeeText('清除') ->assertSeeText('导出') ->assertDontSeeText('该页面暂未配置数据表'); } public function testAlibabaAccountBalanceShowsImportedSnapshotAndFilters(): void { $this->allowMarketingAccountBalancePage(); $repository = Mockery::mock(AlibabaAccountBalanceRepository::class); $repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue(); $repository->shouldReceive('paginateMonthlyBalances') ->once() ->with('阿里测试店', '2026-06', 1, 25) ->andReturn(['rows' => [[ 'date' => '2026-06-30', 'store_name' => '阿里测试店', 'opening_balance' => null, 'income_total' => null, 'expense_total' => null, 'closing_balance' => 4142.96, 'system_closing_balance' => null, 'balance_difference' => null, 'difference_reason' => '', 'verification_status' => '', 'notes' => '', ]], 'total' => 1]); $repository->shouldReceive('storeOptions')->once()->andReturn(['阿里测试店']); $this->app->instance(AlibabaAccountBalanceRepository::class, $repository); $parameters = [ 'section' => 'account-balance-data', 'page' => 'marketing-account-balance', 'tab' => 'taobao-wechat', 'store_name' => '阿里测试店', 'month' => '2026-06', ]; $this->loginAs() ->get(route('business.pages.show', $parameters)) ->assertOk() ->assertSeeText('阿里测试店') ->assertSeeText('4,142.96') ->assertSeeText('期末余额') ->assertSee(e(route('business.pages.export', $parameters)), false) ->assertDontSeeText('该平台余额数据表尚未配置'); } public function testAlibabaAccountBalanceExportsCurrentFilters(): void { $this->allowMarketingAccountBalancePage(); $repository = Mockery::mock(AlibabaAccountBalanceRepository::class); $repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue(); $repository->shouldReceive('paginateMonthlyBalances') ->once() ->with('阿里测试店', '2026-06', 1, 5000) ->andReturn(['rows' => [[ 'date' => '2026-06-30', 'store_name' => '阿里测试店', 'opening_balance' => null, 'income_total' => null, 'expense_total' => null, 'closing_balance' => 4142.96, 'system_closing_balance' => null, 'balance_difference' => null, 'difference_reason' => '', 'verification_status' => '', 'notes' => '', ]], 'total' => 1]); $this->app->instance(AlibabaAccountBalanceRepository::class, $repository); $response = $this->loginAs()->get(route('business.pages.export', [ 'section' => 'account-balance-data', 'page' => 'marketing-account-balance', 'tab' => 'taobao-wechat', 'store_name' => '阿里测试店', 'month' => '2026-06', ])); $response->assertOk()->assertDownload('alibaba_account_balances_2026-06.csv'); $this->assertStringContainsString('阿里测试店', $response->streamedContent()); $this->assertStringContainsString('4142.96', $response->streamedContent()); } public function testJdAccountBalanceUsesTheSharedBalanceTableAndCurrentFilters(): void { $this->allowMarketingAccountBalancePage(); $repository = Mockery::mock(JdPromotionAccountBalanceRepository::class); $repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue(); $repository->shouldReceive('paginateMonthlyBalances') ->once() ->with('京东测试店', '2026-06', 1, 25) ->andReturn(['rows' => [[ 'date' => '2026-06-30', 'store_name' => '京东测试店', 'opening_balance' => 1000.0, 'income_total' => 500.0, 'expense_total' => 200.0, 'closing_balance' => 1300.0, 'system_closing_balance' => 1300.0, 'balance_difference' => 0.0, 'difference_reason' => '', 'verification_status' => '已核对', 'notes' => '', ]], 'total' => 1]); $repository->shouldReceive('storeOptions')->once()->andReturn(['京东测试店']); $this->app->instance(JdPromotionAccountBalanceRepository::class, $repository); $parameters = [ 'section' => 'account-balance-data', 'page' => 'marketing-account-balance', 'tab' => 'jd-wallet', 'store_name' => '京东测试店', 'month' => '2026-06', ]; $this->loginAs() ->get(route('business.pages.show', $parameters)) ->assertOk() ->assertSeeText('京东测试店') ->assertSeeText('期初余额') ->assertSeeText('本期收入') ->assertSeeText('本期支出') ->assertSeeText('期末余额') ->assertSeeText('系统期末余额') ->assertSeeText('余额差异') ->assertSeeText('差异原因') ->assertSeeText('核对状态') ->assertSeeText('1,300.00') ->assertSeeText('已核对') ->assertSee(e(route('business.pages.export', $parameters)), false) ->assertDontSeeText('该平台余额数据表尚未配置'); } public function testJdAccountBalanceExportsTheCurrentFilters(): void { $this->allowMarketingAccountBalancePage(); $repository = Mockery::mock(JdPromotionAccountBalanceRepository::class); $repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue(); $repository->shouldReceive('paginateMonthlyBalances') ->once() ->with('京东测试店', '2026-06', 1, 5000) ->andReturn(['rows' => [[ 'date' => '2026-06-30', 'store_name' => '京东测试店', 'opening_balance' => 1000.0, 'income_total' => 500.0, 'expense_total' => 200.0, 'closing_balance' => 1300.0, 'system_closing_balance' => 1300.0, 'balance_difference' => 0.0, 'difference_reason' => '', 'verification_status' => '已核对', 'notes' => '', ]], 'total' => 1]); $this->app->instance(JdPromotionAccountBalanceRepository::class, $repository); $response = $this->loginAs()->get(route('business.pages.export', [ 'section' => 'account-balance-data', 'page' => 'marketing-account-balance', 'tab' => 'jd-wallet', 'store_name' => '京东测试店', 'month' => '2026-06', ])); $response->assertOk()->assertDownload('jd_account_balances_2026-06.csv'); $content = $response->streamedContent(); $this->assertStringContainsString('京东测试店', $content); $this->assertStringContainsString('2026-06-30', $content); $this->assertStringContainsString('1300', $content); } 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('阿里巴巴账户') ->assertSeeText('京东') ->assertSeeText('导出') ->assertSee(e(route('business.pages.export', [ 'section' => 'billing-transactions', 'page' => 'marketing-account-bills', 'tab' => 'taobao-wechat', ])), false) ->assertDontSeeText('淘系微信账户') ->assertDontSeeText('京东网银钱包账户') ->assertDontSeeText('抖音账户') ->assertDontSeeText('得物账户') ->assertDontSeeText('唯品会账户') ->assertSeeText('抖音千川') ->assertSeeText('得物推广账户') ->assertSeeText('唯品会推广流水') ->assertSee('class="my-6 business-page--marketing-account-bills"', false); } public function testMarketingAccountBillsRemovesDisabledPlatformTabs(): void { $this->allowMarketingAccountBillsPage(); $repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class); $repository->shouldReceive('usesBusinessMirror')->andReturnTrue(); $repository->shouldReceive('paginate')->andReturn(['rows' => [], 'total' => 0]); $repository->shouldReceive('summary')->andReturn([ 'income_total' => 0, 'expense_total' => 0, 'net' => 0, 'count' => 0, ]); $repository->shouldReceive('storeOptions')->andReturn([]); $this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'billing-transactions', 'page' => 'marketing-account-bills', 'tab' => 'taobao-alipay', ])) ->assertOk() ->assertDontSeeText('抖音账户') ->assertDontSeeText('得物账户') ->assertDontSeeText('唯品会账户') ->assertSee('data-marketing-bill-date-filter', false) ->assertSee('data-date-picker', false) ->assertSee('id="tb-filter-from"', false) ->assertSee('name="from"', false) ->assertSeeText('开始日期'); $page = collect(config('business_navigation')) ->firstWhere('slug', 'billing-transactions')['pages']; $marketingBills = collect($page)->firstWhere('slug', 'marketing-account-bills'); $tabSlugs = collect($marketingBills['tabs'])->pluck('slug')->all(); $this->assertNotContains('douyin', $tabSlugs); $this->assertNotContains('dewu', $tabSlugs); $this->assertNotContains('vipshop', $tabSlugs); } public function testPinduoduoBillsPageShowsExportForTheCurrentFilters(): void { $this->allowMarketingAccountBillsPage(); $repository = Mockery::mock(PinduoduoSpreadBillRepository::class); $repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue(); $repository->shouldReceive('paginate')->once()->andReturn(['rows' => [], 'total' => 0]); $repository->shouldReceive('summary')->once()->andReturn([ 'income_total' => 0, 'expense_total' => 0, 'net' => 0, 'count' => 0, ]); $repository->shouldReceive('storeOptions')->once()->andReturn(['筛选店铺']); $this->app->instance(PinduoduoSpreadBillRepository::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); $parameters = [ 'section' => 'billing-transactions', 'page' => 'marketing-account-bills', 'tab' => 'pinduoduo', 'store_name' => '筛选店铺', 'from' => '2026-07-01', 'to' => '2026-07-31', 'flow_type' => 'expense', ]; $this->loginAs() ->get(route('business.pages.show', $parameters)) ->assertOk() ->assertSeeText('导出') ->assertSee(e(route('business.pages.export', $parameters)), false) ->assertSee('aria-label="导出当前筛选结果"', false) ->assertSee('data-marketing-bill-date-filter', false) ->assertSee('id="pdd-filter-from"', false) ->assertSee('data-date-picker', false); } public function testPinduoduoBillsExportUsesTheCurrentMirrorFilters(): void { $this->allowMarketingAccountBillsPage(); $repository = Mockery::mock(PinduoduoSpreadBillRepository::class); $repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue(); $repository->shouldReceive('exportRows') ->once() ->with('筛选店铺', '2026-07-01', '2026-07-31', 'expense') ->andReturn((function (): \Generator { yield [ 'store_name' => '筛选店铺', 'amount' => 12.34, 'balance' => 56.78, 'summary' => '推广支出', 'income_expense_type' => '支出', 'fund_type' => '现金', 'cost_time' => '2026-07-31 23:59:59', ]; })()); $this->app->instance(PinduoduoSpreadBillRepository::class, $repository); $response = $this->loginAs()->get(route('business.pages.export', [ 'section' => 'billing-transactions', 'page' => 'marketing-account-bills', 'tab' => 'pinduoduo', 'store_name' => '筛选店铺', 'from' => '2026-07-01', 'to' => '2026-07-31', 'flow_type' => 'expense', ])); $response->assertOk()->assertDownload('pinduoduo_account_bills_2026-07-01_2026-07-31.csv'); $content = $response->streamedContent(); $this->assertStringContainsString('店铺名称,金额(元),余额(元),摘要,收支类型,资金类型,费用时间', $content); $this->assertStringContainsString('筛选店铺,12.34,56.78,推广支出,支出,现金,"2026-07-31 23:59:59"', $content); } public function testPinduoduoBillsExportExplainsWhenTheMirrorIsNotReady(): void { $this->allowMarketingAccountBillsPage(); $repository = Mockery::mock(PinduoduoSpreadBillRepository::class); $repository->shouldReceive('usesBusinessMirror')->once()->andReturnFalse(); $this->app->instance(PinduoduoSpreadBillRepository::class, $repository); $this->loginAs() ->get(route('business.pages.export', [ 'section' => 'billing-transactions', 'page' => 'marketing-account-bills', 'tab' => 'pinduoduo', 'store_name' => '筛选店铺', ])) ->assertRedirect(route('business.pages.show', [ 'section' => 'billing-transactions', 'page' => 'marketing-account-bills', 'tab' => 'pinduoduo', 'store_name' => '筛选店铺', ])) ->assertSessionHas('error', '拼多多账单共享业务镜像尚未完成同步,暂时不能导出;请稍后重试。'); } public function testTaobaoBillsExportUsesTheCurrentMirrorFilters(): void { $this->allowMarketingAccountBillsPage(); $repository = Mockery::mock(TmallTaobaoSpreadBillRepository::class); $repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue(); $repository->shouldReceive('exportRows') ->once() ->with('淘系筛选店铺', '2026-07-01', '2026-07-31', 'income') ->andReturn((function (): \Generator { yield [ 'store_name' => '淘系筛选店铺', 'bookkeeping_time' => '2026-07-31 09:00:00', 'transaction_date' => '2026-07-31', 'amount' => 12.34, 'balance' => 56.78, 'comment' => '充值', 'fin_type' => 'DEBIT', 'income_expense_type' => '收入', 'transaction_type' => '充值', ]; })()); $this->app->instance(TmallTaobaoSpreadBillRepository::class, $repository); $response = $this->loginAs()->get(route('business.pages.export', [ 'section' => 'billing-transactions', 'page' => 'marketing-account-bills', 'tab' => 'taobao-alipay', 'store_name' => '淘系筛选店铺', 'from' => '2026-07-01', 'to' => '2026-07-31', 'flow_type' => 'income', ])); $response->assertOk()->assertDownload('taobao_account_bills_2026-07-01_2026-07-31.csv'); $content = $response->streamedContent(); $this->assertStringContainsString('店铺名称,记账日期,交易日期,金额(元),余额(元),备注,finType,收支类型,交易类型', $content); $this->assertStringContainsString('淘系筛选店铺,"2026-07-31 09:00:00",2026-07-31,12.34,56.78,充值,DEBIT,收入,充值', $content); } public function testAlibabaBillsExportUsesTheCurrentMirrorFilters(): void { $this->allowMarketingAccountBillsPage(); $repository = Mockery::mock(AlibabaBillRepository::class); $repository->shouldReceive('usesBusinessMirror')->once()->andReturnTrue(); $repository->shouldReceive('exportRows') ->once() ->with('阿里筛选店铺', '全站推广', '2026-07-01', '2026-07-31') ->andReturn((function (): \Generator { yield [ 'shop_name' => '阿里筛选店铺', 'trade_day' => '2026-07-31 10:00:00', 'product_type' => '全站推广', 'income_amount' => 100.25, 'expense_amount' => 20.5, ]; })()); $this->app->instance(AlibabaBillRepository::class, $repository); $response = $this->loginAs()->get(route('business.pages.export', [ 'section' => 'billing-transactions', 'page' => 'marketing-account-bills', 'tab' => 'taobao-wechat', 'shop_name' => '阿里筛选店铺', 'product_type' => '全站推广', 'from' => '2026-07-01', 'to' => '2026-07-31', ])); $response->assertOk()->assertDownload('alibaba_account_bills_2026-07-01_2026-07-31.csv'); $content = $response->streamedContent(); $this->assertStringContainsString('店铺名称,交易时间,产品类型,入账金额(元),出帐金额(元)', $content); $this->assertStringContainsString('阿里筛选店铺,"2026-07-31 10:00:00",全站推广,100.25,20.5', $content); } public function testJdConsumptionBillsExportUsesTheCurrentMirrorFilters(): void { $this->allowMarketingAccountBillsPage(); $repository = Mockery::mock(JdWalletBillRepository::class); $repository->shouldReceive('usesBusinessMirror')->once()->with('consumption')->andReturnTrue(); $repository->shouldReceive('exportRows') ->once() ->with('consumption', '京东筛选店铺', '2026-07-01', '2026-07-31') ->andReturn((function (): \Generator { yield [ 'store_name' => '京东筛选店铺', 'amount' => 88.6, 'balance' => 10.4, 'transaction_type' => '推广消耗', 'investment_date' => '2026-07-30 12:00:00', 'settlement_date' => '2026-07-31 00:00:00', 'serial_number' => 'JD-1', ]; })()); $this->app->instance(JdWalletBillRepository::class, $repository); $response = $this->loginAs()->get(route('business.pages.export', [ 'section' => 'billing-transactions', 'page' => 'marketing-account-bills', 'tab' => 'jd-wallet', 'jd_ledger' => 'consumption', 'store_name' => '京东筛选店铺', 'from' => '2026-07-01', 'to' => '2026-07-31', ])); $response->assertOk()->assertDownload('jd_consumption_account_bills_2026-07-01_2026-07-31.csv'); $content = $response->streamedContent(); $this->assertStringContainsString('店铺名称,金额(元),余额(元),交易类型,投放日期,结算日期,流水号', $content); $this->assertStringContainsString('京东筛选店铺,88.6,10.4,推广消耗,"2026-07-30 12:00:00","2026-07-31 00:00:00",JD-1', $content); } 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('充值时间') ->assertSeeText('导出') ->assertSee('aria-label="导出当前筛选结果"', false) ->assertDontSeeText('投放日期') ->assertSee('data-jd-wallet-panel', false) ->assertSee('data-jd-wallet-navigation', false) ->assertSee('data-marketing-bill-date-filter', false) ->assertSee('id="jd-filter-from"', false) ->assertSee('data-date-picker', false); $this->loginAs() ->get(route('business.pages.show', array_merge($base, ['jd_ledger' => 'consumption']))) ->assertOk() ->assertSeeText('充值账单') ->assertSeeText('消耗账单') ->assertSeeText('金额(元)') ->assertSeeText('投放日期') ->assertSeeText('结算日期') ->assertDontSeeText('充值时间'); } public function testMarketingAccountBillFormsUseTheSharedCalendarDateRange(): void { foreach ([ 'banking/marketing_taobao_alipay_bills/partials/embedded-table.blade.php', 'banking/marketing_alibaba_bills/partials/embedded-table.blade.php', 'banking/marketing_pinduoduo_bills/partials/embedded-table.blade.php', 'banking/marketing_jd_wallet_bills/partials/embedded-table.blade.php', ] as $view) { $this->assertStringContainsString( "@include('business.pages.partials.marketing-bill-date-range'", file_get_contents(resource_path('views/' . $view)), ); } $this->assertStringContainsString( "@include('business.pages.partials.date-range-picker')", file_get_contents(resource_path('views/business/pages/show.blade.php')), ); $this->assertStringContainsString( 'setupBillDatePickers(workspace);', file_get_contents(resource_path('assets/js/views/business/marketing-account-bills.js')), ); } 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' => 'account-balance-data', 'page' => 'marketing-account-balance', 'tab' => 'taobao-alipay', 'store_name' => '测试店铺', 'month' => '2026-08', ])) ->assertRedirect(route('business.pages.show', [ 'section' => 'account-balance-data', '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' => 'account-balance-data', 'page' => 'marketing-account-balance', 'tab' => 'taobao-alipay', ])) ->assertOk() ->assertSeeText('同步请求已进入后台队列,页面会自动检查首份共享数据,无需重复操作。') ->assertSee('data-marketing-balance-pending', false) ->assertSee('window.location.reload()', false) ->assertDontSee('