CaiWuXiTong/tests/Feature/Business/SupplierPayablePagesTest.php

348 lines
14 KiB
PHP

<?php
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use Illuminate\Pagination\LengthAwarePaginator;
use Modules\EcommerceFinance\Repositories\ProductPriceMirrorRepository;
use Modules\EcommerceFinance\Services\JianSunQueryCache;
use Modules\EcommerceFinance\Services\UpstreamQueryCache;
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\Feature\FeatureTestCase;
class SupplierPayablePagesTest extends FeatureTestCase
{
protected function grantAdminPermission(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
}
public function testEverySupplierPayablePageExposesTheFiveRequestedActions(): void
{
$this->grantAdminPermission();
$pages = [
'payment-reconciliation', 'return-reconciliation', 'invoice-deductions',
'compensation-details', 'advance-details', 'sample-deductions',
'other-adjustments', 'delivery-unpriced', 'return-unpriced',
'dropshipping-express', 'board-deductions', 'supplier-payables-report',
];
foreach ($pages as $page) {
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'supplier-payables',
'page' => $page,
]));
$response->assertOk();
foreach (['导入', '导出', '编辑', '查询', '重置'] as $action) {
$response->assertSeeText($action);
}
$response->assertSeeText('下载导入模板');
$response->assertSee('/template', false);
preg_match_all('/class="[^"]*supplier-action-button/', $response->getContent(), $actionButtons);
$this->assertCount(5, $actionButtons[0]);
}
}
public function testSupplierPayableEditorOverridesTheGlobalMultipartRequestType(): void
{
$source = file_get_contents(resource_path('assets/js/views/ecommerce-finance/supplier-payable-maintenance.js'));
$dialog = file_get_contents(resource_path('views/business/pages/supplier-payables/maintenance-toolbar.blade.php'));
$this->assertIsString($source);
$this->assertStringContainsString("'Content-Type': 'application/json'", $source);
$this->assertStringContainsString('maintenanceEditSelected()', $source);
$this->assertStringContainsString('v-if="maintenance.row"', $dialog);
}
public function testVueTablesRequireASelectedRowBeforeToolbarEditing(): void
{
$this->grantAdminPermission();
$pages = [
'payment-reconciliation', 'return-reconciliation', 'invoice-deductions',
'compensation-details', 'advance-details', 'sample-deductions',
'other-adjustments', 'delivery-unpriced', 'return-unpriced',
'dropshipping-express',
];
foreach ($pages as $page) {
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'supplier-payables',
'page' => $page,
]));
$content = $response->getContent();
$response->assertOk();
$this->assertStringContainsString('@current-change="maintenanceSelect"', $content);
$this->assertStringContainsString('@click="maintenanceEditSelected"', $content);
$this->assertStringNotContainsString(':disabled="!maintenance.fields.length', $content);
$this->assertStringNotContainsString('maintenanceOpen(scope.row)', $content);
$this->assertLessThan(strpos($content, '@click="maintenanceEditSelected"'), strpos($content, '重置'));
}
}
public function testDeliveryUnpricedPageUsesItsOwnTwentyColumnTable()
{
$this->grantAdminPermission();
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'supplier-payables',
'page' => 'delivery-unpriced',
]));
$response
->assertOk()
->assertSee('id="delivery-unpriced-app"', false)
->assertSee('来源单号')
->assertSee('单据编号(采购单号)')
->assertSee('负责人')
->assertSee('public/js/ecommerce-finance/delivery-unpriced.min.js', false)
->assertDontSee('class="business-blank-page"', false);
$this->assertSame(20, substr_count($response->getContent(), '<el-table-column'));
}
public function testReturnUnpricedPageUsesItsOwnTwentyColumnTable()
{
$this->grantAdminPermission();
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'supplier-payables',
'page' => 'return-unpriced',
]));
$response
->assertOk()
->assertSee('id="return-unpriced-app"', false)
->assertSee('明细供应商')
->assertSee('供应商条码')
->assertSee('对账人')
->assertSee('public/js/ecommerce-finance/return-unpriced.min.js', false)
->assertDontSee('class="business-blank-page"', false);
$this->assertSame(20, substr_count($response->getContent(), '<el-table-column'));
}
public function testBoardDeductionsPageUsesItsOwnSeventeenColumnTable()
{
$this->grantAdminPermission();
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'supplier-payables',
'page' => 'board-deductions',
]));
$response
->assertOk()
->assertSee('class="supplier-board-table"', false)
->assertSee('入库日期')
->assertSee('出库备注')
->assertSee('送货单号')
->assertDontSee('class="business-blank-page"', false);
preg_match_all('/<th(?:\s|>)/', $response->getContent(), $headers);
$this->assertCount(17, $headers[0]);
}
public function testSupplierPayablesReportPageUsesItsOwnThirtyFourColumnGroupedTable()
{
$this->grantAdminPermission();
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'supplier-payables',
'page' => 'supplier-payables-report',
]));
$response
->assertOk()
->assertSee('class="supplier-payables-report-table"', false)
->assertSee('已销账供应商')
->assertSee('本月采购金额')
->assertSee('<th class="group-addition">加</th>', false)
->assertSee('<th colspan="6" class="group-payment">单位:元</th>', false)
->assertSee('<th class="group-payment">供应商余额</th>', false)
->assertSee('最晚出账单时间(新执行标准)')
->assertDontSee('class="business-blank-page"', false);
preg_match_all('/<col(?:\s|>)/', $response->getContent(), $columns);
$this->assertCount(34, $columns[0]);
}
public function testProductPricesPageUsesOperationsReplicaPriceLimitFields(): void
{
$this->grantAdminPermission();
$repository = \Mockery::mock(ProductPriceMirrorRepository::class);
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('initialized')->once()->andReturnTrue();
$repository->shouldReceive('paginate')
->once()
->withArgs(static function (array $filters, int $page, int $limit): bool {
return $filters === [
'barcode' => 'YMJ005203N1176V01',
'completed_from' => '2025-12-01',
'completed_to' => '2025-12-31',
] && $page === 1 && $limit > 0;
})
->andReturn(new LengthAwarePaginator([(object) [
'upstream_key' => hash('sha256', 'product-price-row'),
'barcode' => 'YMJ005203N1176V01',
'spu' => 'YMJ0052',
'product_name' => '款式二 36/36/176 胡桃木色 BY-008',
'category' => '住宅家具 - 衣帽架',
'production_status' => '销售',
'minimum_price' => 152.8,
'price_limit' => 152.6,
'purchase_price' => 91.11,
'shipping_fee' => 11.62,
'jingxi_limit_price' => 123.0,
'approval_status' => '已完成',
'approval_result' => '已同意',
'workflow_title' => '特殊限价需求申请',
'completed_at' => '2025-12-31 00:00:00',
] ], 1, 25, 1));
$this->app->instance(ProductPriceMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'supplier-payables',
'page' => 'product-prices',
'barcode' => 'YMJ005203N1176V01',
'completed_from' => '2025-12-01',
'completed_to' => '2025-12-31',
]));
$response
->assertOk()
->assertSee('商品编码')
->assertSee('最低售价')
->assertSee('计算限价')
->assertSee('采购价')
->assertSee('京喜限价')
->assertSee('YMJ005203N1176V01')
->assertSee('152.80')
->assertSee('已同意')
->assertSeeText('导入')
->assertSeeText('导出')
->assertSeeText('下载导入模板')
->assertSeeText('查询')
->assertSeeText('重置')
->assertSee('data-product-price-edit-open', false)
->assertSee('data-product-price-row', false)
->assertDontSee('class="product-price-edit-button"', false)
->assertSeeText('编辑商品价格')
->assertDontSee('是否采购降本');
preg_match_all('/<th(?:\s|>)/', $response->getContent(), $headers);
preg_match_all('/class="[^"]*supplier-action-button/', $response->getContent(), $actionButtons);
$this->assertCount(15, $headers[0]);
$this->assertCount(5, $actionButtons[0]);
}
public function testProductPricesPageDoesNotTreatUninitializedMirrorAsEmptyData(): void
{
$this->grantAdminPermission();
$repository = \Mockery::mock(ProductPriceMirrorRepository::class);
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('initialized')->once()->andReturnFalse();
$repository->shouldNotReceive('paginate');
$this->app->instance(ProductPriceMirrorRepository::class, $repository);
$url = route('business.pages.show', [
'company_id' => $this->company->id,
'section' => 'supplier-payables',
'page' => 'product-prices',
]) . '?page=2';
$response = $this->loginAs()->get($url);
$response
->assertOk()
->assertSee('商品价格正在执行首次全量同步')
->assertDontSee('没有符合当前筛选条件的商品价格数据');
}
public function testCompensationPageUsesTheFiveColumnsFromTheReferenceSheet(): void
{
$this->grantAdminPermission();
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'supplier-payables',
'page' => 'compensation-details',
]));
$response
->assertOk()
->assertSee('id="compensation-detail-app"', false)
->assertSee('赔付原因')
->assertSee('备注')
->assertSee('public/js/ecommerce-finance/compensation-details.min.js', false);
$this->assertSame(5, substr_count($response->getContent(), '<el-table-column'));
}
public function testDataBackedSupplierPayableListRoutesAreAvailableToWorkspaceAjaxRequests()
{
$this->grantAdminPermission();
$cache = \Mockery::mock(JianSunQueryCache::class);
$cache->shouldReceive('remember')->times(10)->andReturnUsing(
static fn (string $scope): array => [
'data' => [['scope' => $scope]],
'total' => 1,
],
);
$cache->shouldReceive('responseHeaders')->times(10)->andReturn([]);
$this->app->instance(JianSunQueryCache::class, $cache);
$routes = [
'workspace-data.supplier-payables.payment-reconciliation.index',
'workspace-data.supplier-payables.return-reconciliation.index',
'workspace-data.supplier-payables.invoice-deductions.index',
'workspace-data.supplier-payables.compensation-details.index',
'workspace-data.supplier-payables.advance-details.index',
'workspace-data.supplier-payables.delivery-unpriced.index',
'workspace-data.supplier-payables.return-unpriced.index',
'workspace-data.supplier-payables.sample-deductions.index',
'workspace-data.supplier-payables.other-adjustments.index',
'workspace-data.supplier-payables.dropshipping-express.index',
];
foreach ($routes as $routeName) {
$this->loginAs()
->withHeader('X-Requested-With', 'XMLHttpRequest')
->get(route($routeName))
->assertOk()
->assertJsonPath('total', 1);
}
}
public function testSupplierPayableDataRouteRejectsUsersWithoutTheMatchingSidebarPage(): void
{
$subject = 'user_' . $this->user->id;
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
setting()->set('system.sidebar_permissions', json_encode([
'configured' => [$subject],
'subjects' => [
$subject => ['supplier-payables/invoice-deductions'],
],
], JSON_UNESCAPED_UNICODE));
try {
$this->loginAs()
->withHeader('X-Requested-With', 'XMLHttpRequest')
->get(route('workspace-data.supplier-payables.payment-reconciliation.index'));
$this->fail('缺少页面权限的用户不应读取货款对账数据。');
} catch (HttpException $exception) {
$this->assertSame(403, $exception->getStatusCode());
}
}
}