876 lines
38 KiB
PHP
876 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);
|
|
$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));
|
|
}
|
|
}
|