CaiWuXiTong/tests/Feature/Business/MarketingAccountBillsTabsTest.php
qiuxiang b2647fa26d 新增种菜数据汇总一级菜单并把两个明细页移入
账单流水数据下移出「种菜明细表」「聚水潭订单标签明细」,新增并列一级菜单「种菜数据汇总」(图标 grass)承载这两个页面;导出路由改到 workspace/planting-data/... 并同步路由名、页面中间件与导出链接;更新导航/页面用例(账单流水数据页签列表恢复原样)。
2026-09-30 17:44:06 +08:00

882 lines
38 KiB
PHP

<?php
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use Mockery;
use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException;
use Modules\EcommerceFinance\Repositories\AlibabaAccountBalanceRepository;
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
use Modules\EcommerceFinance\Repositories\JdPromotionAccountBalanceRepository;
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 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('唯品会账户');
$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);
}
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);
$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' => '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('<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' => 'account-balance-data',
'page' => 'marketing-account-balance',
'tab' => 'taobao-alipay',
]))
->assertRedirect(route('business.pages.show', [
'section' => 'account-balance-data',
'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' => 'account-balance-data',
'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 => ['account-balance-data/marketing-account-balance'],
],
], JSON_UNESCAPED_UNICODE));
}
}