CaiWuXiTong/tests/Feature/Business/ManagementCrudTest.php

525 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Feature\Business;
use App\Business\UpstreamBusinessInformation;
use App\Business\StoreReceivingAccountSynchronizer;
use App\Models\Auth\Permission;
use App\Models\Business\InstallationDetail;
use App\Models\Business\StoreReceivingAccount;
use App\Utilities\BusinessNavigation;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Mockery;
use Modules\EcommerceFinance\Repositories\InstallationDetailMirrorRepository;
use Modules\EcommerceFinance\Repositories\JushuitanShopBalanceRepository;
use Tests\Feature\FeatureTestCase;
class ManagementCrudTest extends FeatureTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
}
public function testStoreReceivingAccountPageUsesRebuiltTabsFiltersAndSharedStoreSeed(): void
{
$synchronizer = Mockery::mock(StoreReceivingAccountSynchronizer::class);
$synchronizer->shouldReceive('sync')->once()->with((int) $this->company->id)->andReturn([
'created' => 0, 'updated' => 0, 'skipped' => 0, 'by_platform' => ['天猫' => 1],
]);
$this->app->instance(StoreReceivingAccountSynchronizer::class, $synchronizer);
StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '天猫',
'erp_store_name' => '天猫-测试ERP店',
'shop_name' => '测试店铺名',
'store_status' => '正常',
'store_entity_name' => '测试主体',
'receiving_method' => '支付宝',
]);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'fund-management',
'page' => 'store-receiving-accounts',
'tab' => 'taobao',
]))
->assertOk()
->assertSeeText('淘宝')
->assertSeeText('ERP店铺名称')
->assertSeeText('测试店铺名')
->assertSeeText('测试主体')
->assertSeeText('下载淘宝模板')
->assertSee('aria-label="店铺收款账户筛选"', false)
->assertDontSeeText('安装明细汇总');
}
public function testWithdrawalManagementShowsSafeStoreWithdrawalConfiguration(): void
{
Carbon::setTestNow('2026-09-13 10:00:00');
$store = StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '天猫',
'erp_store_name' => '提现测试ERP店',
'store_id' => 'TM-TEST-1001',
'shop_name' => '提现测试店铺',
'store_entity_name' => '提现测试主体',
'login_password' => 'must-not-be-visible',
'withdrawal_path' => '支付宝',
'withdrawal_account_name' => '测试提现账户',
'bank_card_last_four' => '4321',
]);
StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '京东',
'erp_store_name' => '未接入测试ERP店',
'shop_name' => '未接入测试店铺',
'store_entity_name' => '未接入测试主体',
'withdrawal_path' => '银行卡',
]);
DB::connection('business_mirror')->table(JushuitanShopBalanceRepository::TABLE)->insert([
'balance_date' => '2026-09-12',
'source_scope' => 'all',
'source_platform' => '天猫',
'store_name' => '天猫-提现测试店铺',
'normalized_store_name' => JushuitanShopBalanceRepository::normalizeStoreName('天猫-提现测试店铺'),
'platform_account_id' => 'fixture-account',
'balance' => 19895.01,
'source_row_key' => hash('sha256', (string) $store->id),
'source_file_name' => 'fixture.json',
'source_sha256' => str_repeat('a', 64),
'imported_at' => now(),
'created_at' => now(),
'updated_at' => now(),
]);
$this->assertContains('fund-management/withdrawal-management', \App\Utilities\SidebarPermissions::allKeys());
try {
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'fund-management',
'page' => 'withdrawal-management',
]));
$unmatchedResponse = $this->loginAs()->get(route('business.pages.show', [
'section' => 'fund-management',
'page' => 'withdrawal-management',
'balance_status' => 'unmatched',
]));
} finally {
Carbon::setTestNow();
}
$response
->assertOk()
->assertSeeText('提现管理')
->assertSeeText('提现配置')
->assertSeeText('提现测试ERP店')
->assertSeeText('平台店铺ID')
->assertSeeText('TM-TEST-1001')
->assertSeeText('测试提现账户')
->assertSeeText('**** 4321')
->assertSeeText('9月13日店铺余额')
->assertSeeText('19,895.01')
->assertSeeText('余额源 1 条')
->assertSeeText('已显示余额 1 条')
->assertSeeText('余额状态')
->assertSeeText('预留标准')
->assertSeeText('提现金额')
->assertSeeText('是否达标')
->assertSeeText('待核验')
->assertSee('data-finance-table-name="withdrawal-management-v3"', false)
->assertSeeInOrder([
'<th>店铺平台</th>',
'<th>ERP店铺名称</th>',
'<th>平台店铺ID</th>',
'<th>店铺主体</th>',
'<th>提现路径</th>',
'<th>提现账户名称</th>',
'<th>银行卡号</th>',
'<th class="num" title="余额数据日期:2026-09-12">9月13日店铺余额</th>',
'<th class="num">预留标准</th>',
'<th class="num">提现金额</th>',
'<th>是否达标</th>',
], false)
->assertSee('aria-label="提现管理筛选"', false)
->assertSee('title="聚水潭店铺:天猫-提现测试店铺;余额日期:2026-09-12"', false)
->assertSeeInOrder(['提现测试ERP店', '未接入测试ERP店'])
->assertDontSee('<th>店铺名称</th>', false)
->assertDontSee('<th>银行卡账号(后四位)</th>', false)
->assertDontSee('<th>配置状态</th>', false)
->assertDontSee('<th>操作</th>', false)
->assertDontSeeText('must-not-be-visible');
$unmatchedResponse
->assertOk()
->assertSeeText('未接入测试ERP店')
->assertDontSeeText('提现测试ERP店');
}
public function testInstallationDetailsRendersWorkbookHeadersAndImportedRows(): void
{
$mirror = Mockery::mock(InstallationDetailMirrorRepository::class);
$mirror->shouldReceive('paginate')->once()->andReturn([
'ready' => true, 'total' => 1, 'data' => [array_replace(array_fill_keys(array_keys(InstallationDetail::columnLabels()), null), [
'id' => 1, 'installation_no' => 'INSTALL-TEST-001', 'installation_company' => '测试安装公司',
'store_name' => '测试店铺', 'requested_amount' => '120.50', 'settled_amount' => '100.00', 'item_quantity' => 2,
'related_guid' => 'guid-install-test',
])],
]);
$this->app->instance(InstallationDetailMirrorRepository::class, $mirror);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'financial-operations',
'page' => 'installation-details',
]))
->assertOk()
->assertSeeText('安装明细 · 数据列表')
->assertSeeText('安装单号')
->assertSeeText('申请总金额')
->assertSeeText('商品名称(1)')
->assertSeeText('创建日期:')
->assertSeeText('客服审核时间:')
->assertSeeText('财务审核时间:')
->assertSeeText('推送状态:')
->assertSeeText('选择店铺:')
->assertSeeText('选择删除:')
->assertSeeText('订单类型:')
->assertSeeText('商品ID:')
->assertSeeText('导出')
->assertSeeText('INSTALL-TEST-001')
->assertSeeText('测试安装公司')
->assertSeeText('120.50')
->assertSeeText('共 1 条');
}
public function testInstallationDetailsExportUsesFiltersAndMirrorRows(): void
{
$row = array_fill_keys(array_keys(InstallationDetail::columnLabels()), null);
$row['installation_no'] = 'INSTALL-EXPORT-001';
$mirror = Mockery::mock(InstallationDetailMirrorRepository::class);
$mirror->shouldReceive('paginate')->once()->with(1, 100, ['created_from' => '2026-08-01'])->andReturn([
'ready' => true,
'total' => 1,
'data' => [$row],
]);
$this->app->instance(InstallationDetailMirrorRepository::class, $mirror);
$response = $this->loginAs()->get(route('business.installation-details.export', [
'created_from' => '2026-08-01',
]));
$response->assertOk()->assertHeader('content-type', 'text/csv; charset=UTF-8');
$this->assertStringContainsString('INSTALL-EXPORT-001', $response->streamedContent());
}
public function testInstallationDetailsEndpointReturnsChildItemsForMainRecord(): void
{
$mirror = Mockery::mock(InstallationDetailMirrorRepository::class);
$mirror->shouldReceive('details')->once()->with('guid-install-test')->andReturn([
'ready' => true,
'data' => [[
'transaction_no' => 'INSTALL-TEST-001',
'product_id' => '10035070813217',
'product_name' => '测试商品',
'detail_requested_amount' => '53.59',
'detail_requested_quantity' => 1,
]],
]);
$this->app->instance(InstallationDetailMirrorRepository::class, $mirror);
$this->loginAs()
->get(route('business.installation-details.details', ['guid' => 'guid-install-test']))
->assertOk()
->assertJsonPath('data.0.transaction_no', 'INSTALL-TEST-001')
->assertJsonPath('data.0.product_name', '测试商品');
}
public function testInstallationChildEndpointRequiresTheSameSidebarPermission(): void
{
$this->withExceptionHandling();
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
setting()->set('system.sidebar_permissions', ['configured'=>['user_'.$this->user->id], 'subjects'=>['user_'.$this->user->id=>['store-management/basic-information']]]);
$mirror=Mockery::mock(InstallationDetailMirrorRepository::class);
$mirror->shouldNotReceive('details');
$this->app->instance(InstallationDetailMirrorRepository::class,$mirror);
$this->loginAs()->getJson(route('business.installation-details.details',['guid'=>'guid-install-test']))->assertForbidden();
}
public function testCompanyInformationUsesWorkbookHeadersAndReadOnlyUpstreamRows(): void
{
$this->mockUpstreamPage((object) [
'id' => 1045,
'unified_social_credit_code' => '91320404MADJRTTA0U',
'company_name' => '测试上游公司',
'subscribed_capital' => '10.00',
'company_type' => '店铺-普通公司',
'establishment_date' => '2024-04-28',
'legal_representative' => '测试法人',
'registered_address' => '测试地址',
'business_scope' => '测试经营范围',
'basic_deposit_account_number' => 'J0001',
'company_status' => '正常',
'financial_officer' => '测试财务',
'supervisor' => '',
'taxpayer_category' => '小规模纳税人',
'shareholders' => null,
]);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'company-management',
'page' => 'basic-information',
]))
->assertOk()
->assertSeeText('公司基础信息')
->assertSeeText('认缴注册资本(万元)')
->assertSeeText('基本存款账户信息')
->assertSeeText('纳税人类型')
->assertSeeText('已配置')
->assertSeeText('测试上游公司')
->assertSeeText('既有只读上游')
->assertDontSeeText('导入')
->assertDontSeeText('新增');
}
public function testStoreInformationKeepsTwelveHeadersAndShowsReadOnlyUpstreamRows(): void
{
$this->mockUpstreamPage((object) [
'id' => 2047,
'opened_at' => '2026-07-18',
'erp_store_name' => '测试上游店铺',
'unified_social_credit_code' => '91320412MA216EYU49',
'company_name' => '测试关联公司',
'trademark_number' => '35774784',
'trademark_name' => null,
'platform' => '天猫',
'profit_center' => '运营中心A',
'store_manager' => '测试负责人',
'store_status' => '正常',
'store_type' => '旗舰店',
'channel_mode' => '直销',
]);
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'store-management',
'page' => 'basic-information',
]));
$response
->assertOk()
->assertSeeText('店铺名称')
->assertSeeText('店铺主体')
->assertSeeText('业务部门')
->assertSeeText('测试上游店铺')
->assertSeeText('测试关联公司')
->assertSeeText('既有只读上游')
->assertDontSeeText('导入')
->assertDontSeeText('新增');
preg_match_all('/<th(?:\s|>)/', $response->getContent(), $headers);
$this->assertCount(14, $headers[0]);
}
public function testStoreReportRendersExpandedLifecycleRowsAndClosedInProgressStatus(): void
{
$service = Mockery::mock(UpstreamBusinessInformation::class);
$service->shouldReceive('paginate')->once()->with('store-management', 'store-report', 50)->andReturn(
new LengthAwarePaginator([[
'erp_store_name' => 'ERP测试店铺',
'store_name' => '前台测试店铺',
'store_id' => 'SHOP-001',
'profit_center' => '运营中心A',
'store_manager' => '店铺负责人',
'department_manager' => '部门负责人',
'opened_at' => '2026-05-01',
'status' => '关店中',
'entity' => '测试主体',
'company_type' => '普通公司',
'effective_at' => '2026-07-01',
'invalid_at' => '存续中',
'period_sales' => 25916.80,
'sales_07' => 25916.80,
]], 1, 50, 1, ['path' => '/'])
);
$this->app->instance(UpstreamBusinessInformation::class, $service);
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'store-management',
'page' => 'store-report',
]));
$response
->assertOk()
->assertSeeText('店铺维度报表')
->assertSeeText('ERP测试店铺')
->assertSeeText('前台测试店铺')
->assertSeeText('SHOP-001')
->assertSeeText('测试主体')
->assertSeeText('2026-07-01')
->assertSeeText('存续中')
->assertSeeText('25,916.80')
->assertSeeText('关店中')
->assertSeeText('共享镜像');
preg_match_all('/<th(?:\s|>)/', $response->getContent(), $headers);
$this->assertCount(27, $headers[0]);
}
public function testTrademarkInformationShowsReadOnlyUpstreamRows(): void
{
$this->mockUpstreamPage((object) [
'id' => 600,
'trademark_name' => '测试上游商标',
'trademark_type' => '20',
'trademark_status' => '有效',
'registration_date' => '2025-12-21',
'expiry_date' => '2035-12-20',
'notes' => null,
]);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'trademark-management',
'page' => 'information',
]))
->assertOk()
->assertSeeText('商标名称')
->assertSeeText('注册时间')
->assertSeeText('测试上游商标')
->assertSeeText('既有只读上游')
->assertDontSeeText('导入')
->assertDontSeeText('新增');
}
public function testWorkbookDirectoryReplacesManagementSubmenus(): void
{
$navigation = BusinessNavigation::all();
$sections = collect($navigation)->keyBy('slug');
$this->assertSame(
['subject-operating-report', 'basic-information', 'personnel-inventory', 'company-progress'],
array_column($sections['company-management']['pages'], 'slug')
);
$this->assertSame(
['information', 'trademark-progress', 'rules-information'],
array_column($sections['trademark-management']['pages'], 'slug')
);
$rulesPage = collect($sections['trademark-management']['pages'])->firstWhere('slug', 'rules-information');
$this->assertNotNull($rulesPage);
$this->assertSame(
['subject-trademark-rules', 'store-change-rules'],
array_column($rulesPage['children'], 'slug')
);
$this->assertSame(
['store-report', 'basic-information', 'store-accounts', 'change-progress'],
array_column($sections['store-management']['pages'], 'slug')
);
$this->assertSame(
['todos', 'warnings'],
array_column($sections['other']['pages'], 'slug')
);
foreach (['company-management', 'trademark-management', 'store-management', 'other'] as $sectionSlug) {
foreach ($sections[$sectionSlug]['pages'] as $page) {
$this->assertArrayHasKey('directory', $page, "{$sectionSlug}/{$page['slug']} 缺少目录字段契约");
foreach ($page['directory']['fields'] as $field) {
$this->assertMatchesRegularExpression(
'/\p{Han}/u',
$field['label'],
"{$sectionSlug}/{$page['slug']} 的 {$field['name']} 仍在使用英文表头 {$field['label']}"
);
}
}
}
$this->assertArrayNotHasKey('todos', $sections);
$this->assertArrayNotHasKey('registration', collect($sections['company-management']['pages'])->keyBy('slug'));
$this->assertArrayNotHasKey('changes', collect($sections['trademark-management']['pages'])->keyBy('slug'));
$this->assertArrayNotHasKey('opening-process', collect($sections['store-management']['pages'])->keyBy('slug'));
}
public function testWorkbookDirectoryPagesRenderTheirDeclaredHeaders(): void
{
$this->mockUpstreamPage((object) [
'id' => 1,
'unified_social_credit_code' => null,
'company_name' => null,
'subscribed_capital' => null,
'company_type' => null,
'establishment_date' => null,
'legal_representative' => null,
'registered_address' => null,
'business_scope' => null,
'basic_deposit_account_number' => null,
'company_status' => null,
'financial_officer' => null,
'supervisor' => null,
'taxpayer_category' => null,
'shareholders' => null,
]);
$this->loginAs()
->get(route('business.pages.show', ['section' => 'company-management', 'page' => 'basic-information']))
->assertOk()
->assertSeeText('统一社会信用代码')
->assertSeeText('认缴时间');
$this->mockUpstreamPage((object) [
'id' => 2,
'trademark_name' => null,
'trademark_type' => null,
'trademark_status' => null,
'registration_date' => null,
'expiry_date' => null,
'notes' => null,
]);
$this->get(route('business.pages.show', ['section' => 'trademark-management', 'page' => 'information']))
->assertOk()
->assertSeeText('国际分类')
->assertSeeText('核定使用商品')
->assertSeeText('保管部门');
$expectedHeaders = [
['section' => 'company-management', 'page' => 'subject-operating-report', 'headers' => ['市', '主体', '最近12个月累计收入', '1月收入', '12月收入']],
['section' => 'company-management', 'page' => 'personnel-inventory', 'headers' => ['人员名称', '税务局密码', '可担任公司数量']],
['section' => 'company-management', 'page' => 'company-progress', 'headers' => ['公司进度表字段尚未定义']],
['section' => 'trademark-management', 'page' => 'trademark-progress', 'headers' => ['商标号', '是否完成', '办理人']],
['section' => 'store-management', 'page' => 'store-report', 'headers' => ['ERP系统店铺名称', '店铺名称', '店铺ID(唯一值)', '1月销量', '12月销量']],
['section' => 'store-management', 'page' => 'change-progress', 'headers' => ['接替公司', '完成时间', '办理人']],
['section' => 'other', 'page' => 'todos', 'headers' => ['待办编号', '是否逾期', '关闭日期']],
['section' => 'other', 'page' => 'warnings', 'headers' => ['风险等级', '影响金额', '处理证据']],
];
foreach ($expectedHeaders as $page) {
$response = $this->get(route('business.pages.show', [
'section' => $page['section'],
'page' => $page['page'],
]));
$response->assertOk();
foreach ($page['headers'] as $header) {
$response->assertSeeText($header);
}
}
}
private function mockUpstreamPage(object $row): void
{
$paginator = new LengthAwarePaginator([$row], 1, 50, 1, ['path' => '/']);
$service = Mockery::mock(UpstreamBusinessInformation::class);
$service->shouldReceive('paginate')->atLeast()->once()->andReturn($paginator);
$this->app->instance(UpstreamBusinessInformation::class, $service);
}
}