chenxi 26556a9169 Add warehouse short names to inventory report
Restore warehouse name mapping ledger and inventory short-name column.
2026-09-28 16:33:38 +08:00

1408 lines
64 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\Models\Auth\Permission;
use App\Models\Business\InventoryWarehouseNameMapping;
use App\Business\InventoryWarehouseShortNames;
use App\Jobs\Common\InventoryReportExport;
use Illuminate\Support\Facades\Queue;
use Mockery;
use PhpOffice\PhpSpreadsheet\IOFactory;
use Modules\EcommerceFinance\Repositories\CourierFreightReconciliationMirrorRepository;
use Modules\EcommerceFinance\Repositories\NonJingxiRefundMirrorRepository;
use Modules\EcommerceFinance\Repositories\StoreReceivingAlipayBillMirrorRepository;
use Modules\EcommerceFinance\Repositories\StoreReceivingDouyinBillMirrorRepository;
use Modules\EcommerceFinance\Repositories\ThirdPartyWarehouseStorageMirrorRepository;
use Modules\EcommerceFinance\View\OrderDataPageData;
use Tests\Feature\FeatureTestCase;
class PagesTest extends FeatureTestCase
{
public function testInventoryReportProvidesAFilteredExportButton(): void
{
InventoryWarehouseNameMapping::query()->create([
'company_id' => 1,
'warehouse_provider' => '测试映射仓储方',
'warehouse' => '测试映射仓库',
'warehouse_short_name' => '测试映射简称',
]);
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
$repository->shouldReceive('paginateMonthlyInventoryReport')->once()->with(
[
'month' => '2026-08',
'product_code' => '121630-49',
'warehouse_provider' => '',
'category' => '',
],
1,
Mockery::type('int'),
)->andReturn([
'data' => [(object) [
'product_code' => '121630-49',
'warehouse_provider' => '测试仓储方',
'warehouse' => '测试主仓',
]],
'total' => 1,
'state' => ['status' => 'succeeded'],
'sync' => ['code' => 'healthy'],
]);
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'product-inventory',
'page' => 'inventory-report',
'month' => '2026-08',
'product_code' => '121630-49',
]));
$response->assertOk()
->assertSeeText('导出')
->assertSee('data-inventory-export-scope', false)
->assertSee('data-inventory-export-fields', false)
->assertSee('data-inventory-export', false)
->assertSee('进销存总表')
->assertSee('仓库名称映射表')
->assertSee('测试仓储方')
->assertSee('测试主仓')
->assertSee('data-inventory-report-panel="mapping"', false)
->assertSee('id="inventory-report-total-table"', false)
->assertSee('id="inventory-warehouse-mapping-table"', false)
->assertSee('data-directory-source-url=', false)
->assertSee('mapping=1&amp;page=1', false)
->assertSee('data-directory-table-key="warehouse-mapping"', false)
->assertSee('data-directory-field-picker', false)
->assertSee('data-directory-export-all="visible"', false)
->assertSee('data-directory-export-all="all-fields"', false)
->assertSee('data-inventory-mapping-template', false)
->assertSee('data-directory-fixed="start"', false)
->assertSee('data-directory-fixed="end"', false);
$html = $response->getContent();
$totalPanelStart = strpos($html, 'id="inventory-report-total-panel"');
$mappingPanelStart = strpos($html, 'id="inventory-report-mapping-panel"');
$noticePosition = strpos($html, '已从共享业务镜像读取');
$filterPosition = strpos($html, 'aria-label="进销存报表筛选查询栏"');
$this->assertIsInt($totalPanelStart);
$this->assertIsInt($mappingPanelStart);
$this->assertIsInt($noticePosition);
$this->assertIsInt($filterPosition);
$this->assertGreaterThan($totalPanelStart, $noticePosition);
$this->assertLessThan($mappingPanelStart, $noticePosition);
$this->assertGreaterThan($totalPanelStart, $filterPosition);
$this->assertLessThan($mappingPanelStart, $filterPosition);
}
public function testInventoryReportShowsWarehouseShortNameOnlyForExactProviderAndWarehouse(): void
{
InventoryWarehouseNameMapping::query()->create([
'company_id' => 1,
'warehouse_provider' => '测试仓储方',
'warehouse' => '测试主仓',
'warehouse_short_name' => '崔桥主仓',
]);
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
$repository->shouldReceive('paginateMonthlyInventoryReport')->once()->andReturn([
'data' => [
(object) ['warehouse_provider' => '测试仓储方', 'warehouse' => '测试主仓', 'product_code' => 'MATCHED'],
(object) ['warehouse_provider' => '其他仓储方', 'warehouse' => '测试主仓', 'product_code' => 'UNMATCHED'],
],
'total' => 2,
'state' => ['status' => 'succeeded'],
'sync' => ['code' => 'healthy'],
]);
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'product-inventory', 'page' => 'inventory-report', 'month' => '2026-08',
]));
$response->assertOk()
->assertSee('data-column-key="warehouse_short_name"', false)
->assertSee('data-inventory-short-name-sort', false)
->assertSee('value="warehouse_short_name"', false)
->assertSee('崔桥主仓');
$html = $response->getContent();
$this->assertLessThan(
strpos($html, 'data-column-key="warehouse_short_name"'),
strpos($html, 'data-column-key="warehouse"'),
);
$this->assertMatchesRegularExpression('/data-column-key="warehouse_short_name"[^>]*>\s*崔桥主仓\s*<\/td>/', $html);
$this->assertMatchesRegularExpression('/data-column-key="warehouse_short_name"[^>]*>\s*<\/td>/', $html);
}
public function testInventoryReportKeepsJulyMonthFilter(): void
{
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
$repository->shouldReceive('paginateMonthlyInventoryReport')->once()->with(
[
'month' => '2026-07',
'product_code' => '',
'warehouse_provider' => '',
'category' => '',
],
1,
Mockery::type('int'),
)->andReturn([
'data' => [],
'total' => 0,
'state' => ['status' => 'succeeded'],
'sync' => ['code' => 'healthy'],
]);
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'product-inventory',
'page' => 'inventory-report',
'month' => '2026-07',
]))->assertOk()
->assertSee('min="2026-07"', false)
->assertSee('value="2026-07"', false)
->assertSee('data-inventory-month-picker', false)
->assertSee('data-inventory-month-grid', false)
->assertSee('data-inventory-month-current', false);
}
public function testInventoryReportExportUsesCurrentFiltersAndPageColumnOrder(): void
{
$filters = [
'month' => '2026-08',
'product_code' => '121630-49',
'warehouse_provider' => '测试仓',
'category' => '家具',
];
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
$repository->shouldReceive('paginateMonthlyInventoryReport')->once()->with(
$filters,
1,
Mockery::type('int'),
false,
)->andReturn([
'data' => [(object) [
'owner' => '常州简尚家居有限公司',
'month' => '2026-08-01',
'warehouse_provider' => '测试仓',
'warehouse' => '测试仓',
'product_code' => '121630-49',
'category' => '家具',
'opening_quantity' => 10,
'closing_quantity' => 12,
]],
]);
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route(
'business.product-inventory.inventory-report.export',
$filters + ['scope' => 'page'],
));
$response->assertOk()->assertDownload()->assertHeader('X-Accel-Buffering', 'no');
$content = $response->streamedContent();
$this->assertSame((string) strlen($content), $response->headers->get('Content-Length'));
$this->assertStringStartsWith(chr(0xEF) . chr(0xBB) . chr(0xBF), $content);
$this->assertStringContainsString('货主,月份,仓储方,仓库,仓库简称,商品编码,分类,期初数量,期末数量', $content);
$this->assertStringContainsString("'121630-49", $content);
}
public function testInventoryReportExportUsesSelectedVisibleColumnsInOrder(): void
{
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
$repository->shouldReceive('paginateMonthlyInventoryReport')->once()->with(
[
'month' => '2026-08', 'product_code' => '121630-49',
'warehouse_provider' => '', 'category' => '',
],
1,
Mockery::type('int'),
false,
)->andReturn([
'data' => [(object) ['product_code' => '121630-49', 'warehouse' => '主仓', 'opening_quantity' => 10]],
]);
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('business.product-inventory.inventory-report.export', [
'month' => '2026-08', 'product_code' => '121630-49',
'columns' => 'product_code,warehouse,opening_quantity', 'scope' => 'page',
]));
$response->assertOk()->assertDownload();
$this->assertSame("\xEF\xBB\xBF商品编码,仓库,期初数量\n'121630-49,主仓,10\n", $response->streamedContent());
}
public function testInventoryReportExportIncludesMappedShortNameAndLeavesUnmatchedBlank(): void
{
InventoryWarehouseNameMapping::query()->create([
'company_id' => 1,
'warehouse_provider' => '测试仓储方',
'warehouse' => '主仓',
'warehouse_short_name' => '测试简称',
]);
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
$repository->shouldReceive('paginateMonthlyInventoryReport')->once()->andReturn([
'data' => [
(object) ['warehouse_provider' => '测试仓储方', 'warehouse' => '主仓'],
(object) ['warehouse_provider' => '其他仓储方', 'warehouse' => '主仓'],
],
]);
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('business.product-inventory.inventory-report.export', [
'month' => '2026-08', 'scope' => 'page',
'columns' => 'warehouse,warehouse_short_name,warehouse_provider',
]));
$response->assertOk()->assertDownload();
$this->assertSame(
"\xEF\xBB\xBF仓库,仓库简称,仓储方\n主仓,测试简称,测试仓储方\n主仓,,其他仓储方\n",
$response->streamedContent(),
);
}
public function testInventoryReportCurrentPageExportDoesNotReadAllFilteredRows(): void
{
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
$repository->shouldNotReceive('exportMonthlyInventoryReportRows');
$repository->shouldReceive('paginateMonthlyInventoryReport')->once()->with(
['month' => '2026-08', 'product_code' => '', 'warehouse_provider' => '', 'category' => ''],
2,
Mockery::type('int'),
false,
)->andReturn(['data' => [(object) ['product_code' => 'PAGE-2']], 'total' => 0]);
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('business.product-inventory.inventory-report.export', [
'month' => '2026-08', 'page' => 2, 'scope' => 'page', 'columns' => 'product_code',
]));
$response->assertOk()->assertDownload();
$this->assertStringContainsString("商品编码\n'PAGE-2\n", $response->streamedContent());
}
public function testInventoryReportFullExportUsesTheQueueWhenEnabled(): void
{
Queue::fake();
config(['queue.default' => 'database']);
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
$repository->shouldNotReceive('exportMonthlyInventoryReportRows');
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.product-inventory.inventory-report.export', [
'month' => '2026-08',
'scope' => 'all',
]))->assertRedirect();
Queue::assertPushed(InventoryReportExport::class);
}
public function testInventoryReportFullExportDefersAfterResponseWhenQueueIsDisabled(): void
{
Queue::fake();
config(['queue.default' => 'sync']);
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
$repository->shouldNotReceive('exportMonthlyInventoryReportRows');
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
$this->loginAs()->get(route('business.product-inventory.inventory-report.export', [
'month' => '2026-08',
'scope' => 'all',
]))->assertRedirect();
Queue::assertPushed(InventoryReportExport::class);
}
public function testInventoryReportBackgroundExportWritesWarehouseShortName(): void
{
Queue::fake();
InventoryWarehouseNameMapping::query()->create([
'company_id' => 1,
'warehouse_provider' => '后台仓储方',
'warehouse' => '后台仓库',
'warehouse_short_name' => '后台简称',
]);
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
$repository->shouldReceive('exportMonthlyInventoryReportRows')->once()->andReturn(
(static function (): \Generator {
yield (object) ['warehouse_provider' => '后台仓储方', 'warehouse' => '后台仓库'];
})(),
);
$fileName = 'inventory-short-name-test-' . uniqid() . '.csv';
$path = storage_path('app/temp/' . $fileName);
try {
(new InventoryReportExport(
$this->user,
['month' => '2026-08'],
[['name' => 'warehouse_short_name', 'label' => '仓库简称']],
$fileName,
1,
))->handle($repository, app(InventoryWarehouseShortNames::class));
$this->assertSame("\xEF\xBB\xBF仓库简称\n后台简称\n", file_get_contents($path));
} finally {
@unlink($path);
}
}
public function testInventoryReportExportRejectsFieldsOutsideThePageSchema(): void
{
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
$repository->shouldNotReceive('exportMonthlyInventoryReportRows');
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
$this->withExceptionHandling();
$this->loginAs()->get(route('business.product-inventory.inventory-report.export', [
'columns' => 'product_code,password',
]))->assertStatus(422);
}
public function testInventoryReportDoesNotFallBackToLegacyRowsBeforeAllWarehouseMirrorIsReady(): void
{
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('allWarehousesReady')->once()->andReturnFalse();
$repository->shouldNotReceive('paginateMonthlyInventoryReport');
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'product-inventory',
'page' => 'inventory-report',
]))
->assertOk()
->assertSeeText('进销存全仓库共享镜像正在执行首次同步')
->assertDontSeeText('已从共享业务镜像读取');
}
public function testFreightReconciliationPageRendersSharedMirrorRowsAndSummary(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$repository = Mockery::mock(CourierFreightReconciliationMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnTrue();
$repository->shouldReceive('ready')->once()->andReturnTrue();
$repository->shouldReceive('paginate')->once()->with(
Mockery::on(static fn (array $filters): bool => $filters['express_no'] === 'FIXTURE'),
1,
Mockery::type('int'),
)->andReturn([
'data' => [[
'detail_id' => 1,
'waybill' => 'FIXTURE-WAYBILL', 'product_id' => 'ITEM-1',
'send_time' => '2026-08-01 09:00:00', 'transaction_order_no' => 'SO-1',
'province' => '江苏省', 'city' => '常州市', 'district' => '武进区', 'barcode' => 'SKU-1',
'express_name' => '测试快递', 'express_name_2' => '测试快递', 'quantity' => 1,
'product_name' => '测试商品', 'platform' => '淘宝天猫', 'shop_name' => '测试店铺',
'warehouse' => '测试仓库', 'volume' => 1.2, 'supplier' => '测试供应商',
'length' => 1.0, 'width' => 2.0, 'height' => 3.0, 'freight' => 20.0,
'weight' => 1.25, 'reconciliation_weight' => 1.25, 'reconciliation_volume' => 1.2,
'month' => '2026-08', 'status' => '已发货',
]],
'total' => 1,
'summary' => ['count' => 1, 'freight' => 20.0, 'payable_amount' => 20.0, 'actual_payment' => 0.0, 'confirmed_amount' => 0.0, 'reconciliation_difference' => 0.0],
'state' => ['status' => 'succeeded'],
'sync' => ['code' => 'fresh', 'last_succeeded_at' => '2026-08-02 09:00:00', 'lag_minutes' => 1],
]);
$this->app->instance(CourierFreightReconciliationMirrorRepository::class, $repository);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'freight-reconciliation',
'express_no' => 'FIXTURE',
]))
->assertOk()
->assertSeeText('FIXTURE-WAYBILL')
->assertSeeText('条形码')
->assertSeeText('交易订单号')
->assertSeeText('对账体积(cm3)')
->assertSeeText('SKU-1')
->assertSeeText('测试供应商')
->assertSeeText('运费合计')
->assertSeeText('20.00')
->assertSeeText('数据新鲜')
->assertSeeText('共享业务镜像')
->assertDontSee('class="business-blank-page"', false);
}
public function testTaobaoAlipayBillsEndpointIsAvailableToWorkspaceUsers()
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$repository = Mockery::mock(StoreReceivingAlipayBillMirrorRepository::class);
$repository->shouldReceive('paginate')->once()->andReturn([
'current_page' => 1,
'last_page' => 1,
'per_page' => 10,
'total' => 1,
'data' => [[
'record_code' => 'time-format-test',
'occurred_at' => '2026-07-15 23:59:12',
]],
'summary' => ['income' => 1.0, 'expense' => 0.0, 'count' => 1, 'balance' => 1.0],
'sync' => ['status' => 'succeeded', 'last_succeeded_at' => '2026-09-07 09:00:00'],
]);
$this->app->instance(StoreReceivingAlipayBillMirrorRepository::class, $repository);
$this->loginAs()
->withHeader('X-Requested-With', 'XMLHttpRequest')
->get(route('ecommerce-finance.alipay-bills.index'))
->assertOk()
->assertJsonStructure([
'current_page',
'data',
'total',
'summary' => ['income', 'expense', 'count', 'balance'],
])
->assertJsonPath('data.0.occurred_at', '2026-07-15 23:59:12');
}
public function testTaobaoAlipayFilterOptionsUseTheSameMirrorScope(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$repository = Mockery::mock(StoreReceivingAlipayBillMirrorRepository::class);
$repository->shouldReceive('filterOptionsForChannel')->once()->with(
'alipayCSV', StoreReceivingAlipayBillMirrorRepository::DATASET, true
)->andReturn(['shops' => ['天猫A店'], 'bill_items' => ['交易收款']]);
$this->app->instance(StoreReceivingAlipayBillMirrorRepository::class, $repository);
$this->loginAs()
->getJson(route('ecommerce-finance.alipay-bills.filter-options'))
->assertOk()
->assertExactJson(['shops' => ['天猫A店'], 'bill_items' => ['交易收款']]);
}
public function testTaobaoAlipayFilterOptionsReportAnUnreadyMirror(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$repository = Mockery::mock(StoreReceivingAlipayBillMirrorRepository::class);
$repository->shouldReceive('filterOptionsForChannel')->once()
->andThrow(new \RuntimeException('store_receiving_alipay_bill_mirror_not_ready'));
$this->app->instance(StoreReceivingAlipayBillMirrorRepository::class, $repository);
$this->loginAs()
->getJson(route('ecommerce-finance.alipay-bills.filter-options'))
->assertStatus(503)
->assertJsonPath('shops', [])
->assertJsonPath('bill_items', []);
}
public function testTaobaoAlipayBillsPageProvidesTheVueMountPoint()
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'billing-transactions',
'page' => 'store-receiving-bills',
'tab' => 'taobao-alipay',
]))
->assertOk()
->assertSee('class="biz-card mt-6 business-blank-page"', false)
->assertSee('public/js/business/pages.min.js', false);
}
public function testDouyinWorkspaceUsesMatchingRuntimeAssetVersions(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs()->get(route('business.pages.show', [
'section' => 'billing-transactions', 'page' => 'store-receiving-bills', 'tab' => 'douyin',
]))->assertOk()
->assertSee('business-blank-page', false)
->assertSee('public/js/manifest.js?id=' . md5_file(public_path('js/manifest.js')), false)
->assertSee('public/js/vendor.js?id=' . md5_file(public_path('js/vendor.js')), false)
->assertSee('public/js/business/pages.min.js?id=' . md5_file(public_path('js/business/pages.min.js')), false)
->assertDontSee('public/js/vendor.js?v=', false);
}
public function testDouyinAccountBillsEndpointReadsTheFixedTouTiaoChannel(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$repository = Mockery::mock(StoreReceivingDouyinBillMirrorRepository::class);
$repository->shouldReceive('paginate')->once()->with(
[], 'occurred_at', 'desc', 1, 10,
)->andReturn([
'current_page' => 1,
'last_page' => 1,
'per_page' => 10,
'total' => 1,
'data' => [[
'id' => '9001',
'record_code' => '9001',
'store_name' => '放心购旗舰店',
'income' => 88.50,
'expense' => 0.0,
'payload' => [
'settle_account' => 'TouTiaoFXGZJ',
'move_scene' => '头条放心购',
],
]],
'summary' => ['income' => 88.50, 'expense' => 0.0, 'balance' => 88.50, 'count' => 1],
'sync' => ['status' => 'succeeded', 'last_succeeded_at' => '2026-09-08 09:00:00'],
]);
$this->app->instance(StoreReceivingDouyinBillMirrorRepository::class, $repository);
$this->loginAs()
->withHeader('X-Requested-With', 'XMLHttpRequest')
->get(route('ecommerce-finance.account-bills.index', ['bill_type' => 'douyin']))
->assertOk()
->assertJsonPath('data.0.payload.move_scene', '头条放心购')
->assertJsonPath('data.0.payload.settle_account', 'TouTiaoFXGZJ')
->assertJsonPath('columns.title', '抖音账户')
->assertJsonPath('summary.count', 1);
}
public function testDouyinAccountBillsEndpointPassesAllFiltersToTheMirror(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$filters = [
'store_name' => '放心购',
'bill_item' => '交易佣',
'start' => '2026-08-01',
'end' => '2026-08-01',
'keyword' => '',
];
$repository = Mockery::mock(StoreReceivingDouyinBillMirrorRepository::class);
$repository->shouldReceive('paginate')->once()->with(
$filters, 'occurred_at', 'desc', 1, 10,
)->andReturn([
'current_page' => 1,
'last_page' => 1,
'per_page' => 10,
'total' => 0,
'data' => [],
'summary' => ['income' => 0.0, 'expense' => 0.0, 'balance' => 0.0, 'count' => 0],
]);
$this->app->instance(StoreReceivingDouyinBillMirrorRepository::class, $repository);
$this->loginAs()
->withHeader('X-Requested-With', 'XMLHttpRequest')
->get(route('ecommerce-finance.account-bills.index', [
'bill_type' => 'douyin',
...$filters,
]))
->assertOk()
->assertJsonPath('total', 0);
}
public function testDouyinAccountBillsFilterOptionsEndpointReturnsUsableChoices(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$repository = Mockery::mock(StoreReceivingDouyinBillMirrorRepository::class);
$repository->shouldReceive('filterOptions')->once()->andReturn([
'shops' => ['放心购旗舰店', '放心购专营店'],
'bill_items' => ['订单收入', '交易佣金'],
]);
$this->app->instance(StoreReceivingDouyinBillMirrorRepository::class, $repository);
$this->loginAs()
->withHeader('X-Requested-With', 'XMLHttpRequest')
->get(route('ecommerce-finance.account-bills.filter-options', ['bill_type' => 'douyin']))
->assertOk()
->assertExactJson([
'shops' => ['放心购旗舰店', '放心购专营店'],
'bill_items' => ['订单收入', '交易佣金'],
]);
}
public function testDouyinAccountBillsExportUsesDateFiltersAndSelectedColumnOrder(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$repository = Mockery::mock(StoreReceivingDouyinBillMirrorRepository::class);
$repository->shouldReceive('exportRows')->once()->with([
'start' => '2026-09-09',
'end' => '2026-09-09',
], 'occurred_at', 'desc')->andReturn([[
'record_code' => '9001',
'payload' => [
'move_scene' => '头条放心购',
'settle_account' => 'TouTiaoFXGZJ',
],
]]);
$this->app->instance(StoreReceivingDouyinBillMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('ecommerce-finance.account-bills.export', [
'bill_type' => 'douyin',
'start' => '2026-09-09',
'end' => '2026-09-09',
'columns' => 'payload.move_scene,record_code,payload.settle_account',
]));
$response->assertOk()
->assertHeader('content-type', 'text/csv; charset=UTF-8')
->assertDownload('douyin_account_bills.csv');
$csv = $response->streamedContent();
$this->assertStringContainsString('动账场景,记录编码,结算账户', $csv);
$this->assertStringContainsString('头条放心购,9001,TouTiaoFXGZJ', $csv);
}
public function testDouyinAccountBillsReturnsARetryableResponseWhenMirrorIsNotReady(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$repository = Mockery::mock(StoreReceivingDouyinBillMirrorRepository::class);
$repository->shouldReceive('paginate')->once()->andThrow(
new \RuntimeException('store_receiving_alipay_bill_mirror_not_ready')
);
$this->app->instance(StoreReceivingDouyinBillMirrorRepository::class, $repository);
$this->loginAs()
->withHeader('X-Requested-With', 'XMLHttpRequest')
->get(route('ecommerce-finance.account-bills.index', ['bill_type' => 'douyin']))
->assertStatus(503)
->assertJsonPath('message', '抖音账户账单共享业务镜像正在首次同步,完成后将自动显示。');
}
public function testTaobaoAlipayBillsExportUsesTheMirrorRepository()
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$repository = Mockery::mock(StoreReceivingAlipayBillMirrorRepository::class);
$repository->shouldReceive('exportRows')->once()->with(Mockery::on(
static fn (array $filters): bool => ($filters['store_name'] ?? '') === '天猫-经营店'
&& ($filters['start'] ?? '') === '2026-08-01'
&& ($filters['end'] ?? '') === '2026-08-31'
), 'occurred_at', 'desc')->andReturn([[
'record_code' => '1', 'store_name' => '天猫-经营店', 'alipay_account' => 'CommonBillTGCPay',
'bill_item' => '交易佣金', 'account_order_no' => 'KEY-1', 'business_serial_no' => '1001',
'merchant_order_no' => 'P-1', 'business_base_order_no' => 'ORDER-1', 'goods_name' => 'SKU-1',
'occurred_at' => '2026-08-01 09:00:00', 'income' => 100.25, 'expense' => 0,
'balance' => 100.25, 'business_type' => '收入', 'online_order_no' => 'ORDER-1',
'business_description' => 'double', 'business_bill_source' => 'CommonBillTGCPay',
'created_at' => '2026-08-02 09:00:00', 'updated_at' => '2026-08-02 10:00:00',
]]);
$this->app->instance(StoreReceivingAlipayBillMirrorRepository::class, $repository);
$this->loginAs()
->get(route('ecommerce-finance.alipay-bills.export', [
'store_name' => '天猫-经营店', 'start' => '2026-08-01', 'end' => '2026-08-31',
]))
->assertOk()
->assertHeader('content-type', 'text/csv; charset=UTF-8')
->assertDownload('taobao_alipay_bills.csv');
}
public function testSupplierPayablesPageRendersItsSpecificContent()
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'supplier-payables',
'page' => 'payment-reconciliation',
]))
->assertOk()
->assertSee('id="goods-reconciliation-app"', false)
->assertSee('public/js/ecommerce-finance/goods-reconciliations.min.js', false)
->assertDontSee('class="business-blank-page"', false);
}
public function testOrderDataTablesRenderAsRawDataPageTabs()
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$this->loginAs()
->get(route('business.pages.show', ['section' => 'order-data']))
->assertRedirect(route('business.pages.show', [
'section' => 'order-data',
'page' => 'raw-data',
'tab' => 'after-sales-expenses',
]));
$this->get(route('business.pages.show', [
'section' => 'order-data',
'page' => 'raw-data',
]))
->assertRedirect(route('business.pages.show', [
'section' => 'order-data',
'page' => 'raw-data',
'tab' => 'after-sales-expenses',
]));
$this->get(route('business.pages.show', [
'section' => 'order-data',
'page' => 'raw-data',
'tab' => 'after-sales-expenses',
]))
->assertOk()
->assertSeeText('订单售后费用明细')
->assertSee('aria-label="订单售后费用明细"', false)
->assertSeeText('内部订单号')
->assertSeeText('800503发空包/赠品费用')
->assertSee('class="business-tab-bar"', false)
->assertSee(route('business.pages.show', [
'section' => 'order-data',
'page' => 'raw-data',
'tab' => 'after-sales-expenses',
]), false)
->assertSee(route('business.pages.show', [
'section' => 'order-data',
'page' => 'raw-data',
'tab' => 'promotion-expenses',
]), false)
->assertDontSee(route('business.pages.show', [
'section' => 'order-data',
'page' => 'after-sales-expenses',
]), false)
->assertDontSee('class="business-blank-page"', false);
$this->get(route('business.pages.show', [
'section' => 'order-data',
'page' => 'raw-data',
'tab' => 'promotion-expenses',
]))
->assertOk()
->assertSeeText('商品推广费用明细')
->assertSee('aria-label="商品推广费用明细"', false)
->assertSeeText('店铺')
->assertSeeText('8001201直通车/钻展/快车等')
->assertDontSee('class="business-blank-page"', false);
}
public function testOrderDataIncludesTheConfiguredBillOffsetSubpage(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$filters = [
'from' => now()->subDays(6)->toDateString(), 'to' => now()->toDateString(),
'order_no' => '', 'shop_name' => '', 'sku_id' => '',
];
$orderData = Mockery::mock(OrderDataPageData::class);
$orderData->shouldReceive('afterSalesFilters')->zeroOrMoreTimes()->andReturn($filters);
$orderData->shouldReceive('afterSalesRows')->zeroOrMoreTimes()->andReturn([
'rows' => [], 'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'],
]);
$this->app->instance(OrderDataPageData::class, $orderData);
$this->loginAs();
$this->get(route('business.pages.show', [
'section' => 'order-data',
'page' => 'bill-offset',
]))
->assertOk()
->assertSeeInOrder([
route('business.pages.show', [
'section' => 'order-data',
'page' => 'raw-data',
'tab' => 'after-sales-expenses',
]),
route('business.pages.show', [
'section' => 'order-data',
'page' => 'bill-offset',
]),
], false);
$this->get(route('business.pages.show', [
'section' => 'order-data',
'page' => 'bill-offset',
]))
->assertOk()
->assertSeeText('账单冲减')
->assertSeeText('暂无数据')
->assertDontSeeText('账单冲减镜像尚未完成首次同步');
$this->withExceptionHandling();
foreach (['national-subsidy', 'hundred-subsidy', 'consumption-coupon'] as $removedSlug) {
$this->withHeader('X-Requested-With', 'XMLHttpRequest')
->get(route('business.pages.show', [
'section' => 'order-data',
'page' => $removedSlug,
]))
->assertNotFound();
}
}
public function testBillOffsetPageUsesAfterSalesRowsAndTheOriginalAccountField(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$filters = [
'from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => '', 'shop_name' => '', 'sku_id' => '',
];
$orderData = Mockery::mock(OrderDataPageData::class);
$orderData->shouldReceive('afterSalesFilters')->once()->andReturn($filters);
$orderData->shouldReceive('afterSalesRows')->once()->with($filters, 1, 25)->andReturn([
'rows' => [[
'internal_order_no' => '9228703', 'online_order_no' => '5127724585360006527',
'shop_name' => '天猫-亿家达旗舰店', 'shop_code' => 'SHOP-1', 'product_code' => 'DNZ005374N79V01',
'style_code' => 'STYLE-1', 'shop_product_code' => 'SP-1', 'shop_style_code' => 'SI-1', 'account_500112' => -20,
]],
'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'],
]);
$this->app->instance(OrderDataPageData::class, $orderData);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'order-data', 'page' => 'bill-offset', 'from' => '2026-08-01', 'to' => '2026-08-31',
]))->assertOk()
->assertSeeText('9228703')
->assertSeeText('天猫-亿家达旗舰店')
->assertSeeText('-20.00')
->assertSeeText('本页 500112 冲减收入合计')
->assertSee(e(route('business.bill-offset.export', [
'from' => '2026-08-01',
'to' => '2026-08-31',
])), false)
->assertSee('id="bill-offset-export-button"', false)
->assertSeeText('数据状态:DIRECT');
}
public function testBillOffsetPageKeepsEmptyAccountFieldAsDash(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$filters = [
'from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => '', 'shop_name' => '', 'sku_id' => '',
];
$orderData = Mockery::mock(OrderDataPageData::class);
$orderData->shouldReceive('afterSalesFilters')->once()->andReturn($filters);
$orderData->shouldReceive('afterSalesRows')->once()->with($filters, 1, 25)->andReturn([
'rows' => [[
'internal_order_no' => 'ORDER-EMPTY', 'online_order_no' => 'ONLINE-EMPTY',
'shop_name' => '空值店铺', 'account_500112' => null,
]],
'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'],
]);
$this->app->instance(OrderDataPageData::class, $orderData);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'order-data', 'page' => 'bill-offset', 'from' => '2026-08-01', 'to' => '2026-08-31',
]))->assertOk()
->assertSeeText('ORDER-EMPTY')
->assertSeeText('空值店铺')
->assertSeeText('本页 500112 冲减收入合计')
->assertSee('data-export-value=""', false)
->assertSeeText('-')
->assertDontSeeText('0.00');
}
public function testBillOffsetPageShowsReadFailureSeparatelyFromAnUnreadyMirror(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$filters = [
'from' => now()->subDays(6)->toDateString(), 'to' => now()->toDateString(),
'order_no' => '', 'shop_name' => '', 'sku_id' => '',
];
$orderData = Mockery::mock(OrderDataPageData::class);
$orderData->shouldReceive('afterSalesFilters')->once()->andReturn($filters);
$orderData->shouldReceive('afterSalesRows')->once()->andThrow(new \RuntimeException('database unavailable'));
$this->app->instance(OrderDataPageData::class, $orderData);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'order-data', 'page' => 'bill-offset',
]))->assertOk()
->assertSeeText('订单售后费用数据暂时无法读取,请稍后重试。');
}
public function testBillOffsetExportKeepsUnknownAccountAmountsEmpty(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$filters = [
'from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => 'ORDER-EMPTY', 'shop_name' => '', 'sku_id' => '',
];
$orderData = Mockery::mock(OrderDataPageData::class);
$orderData->shouldReceive('afterSalesFilters')->once()->with($filters)->andReturn($filters);
$orderData->shouldReceive('afterSalesRows')->once()->with($filters, 1, 100)->andReturn([
'rows' => [[
'internal_order_no' => 'ORDER-EMPTY', 'account_500112' => null, 'reversal_order_amount' => 987.65,
]],
'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'],
]);
$this->app->instance(OrderDataPageData::class, $orderData);
$csv = $this->loginAs()->get(route('business.bill-offset.export', $filters))->streamedContent();
$this->assertStringNotContainsString('987.65', $csv);
$lines = preg_split('/\r?\n/', trim($csv));
$this->assertSame('', str_getcsv($lines[1])[8]);
}
public function testBillOffsetShowsTheOriginalDataErrorAndBlocksAnEmptyExport(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$filters = ['from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => '', 'shop_name' => '', 'sku_id' => ''];
$orderData = Mockery::mock(OrderDataPageData::class);
$orderData->shouldReceive('afterSalesFilters')->twice()->andReturn($filters);
$orderData->shouldReceive('afterSalesRows')->twice()->andThrow(new \RuntimeException('database unavailable'));
$this->app->instance(OrderDataPageData::class, $orderData);
$this->loginAs()->get(route('business.pages.show', ['section' => 'order-data', 'page' => 'bill-offset']))
->assertOk()->assertSeeText('订单售后费用数据暂时无法读取,请稍后重试。');
$this->withExceptionHandling()->get(route('business.bill-offset.export'))->assertStatus(503);
}
public function testBillOffsetExportIncludesAllPagesFromOriginalDataWithTheSameFilters(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$filters = ['from' => '2026-08-01', 'to' => '2026-08-31', 'order_no' => 'ORDER', 'shop_name' => '', 'sku_id' => ''];
$orderData = Mockery::mock(OrderDataPageData::class);
$orderData->shouldReceive('afterSalesFilters')->once()->with($filters)->andReturn($filters);
$orderData->shouldReceive('afterSalesRows')->once()->with($filters, 1, 100)->andReturn([
'rows' => [[
'internal_order_no' => 'ORDER-1', 'online_order_no' => '001234567890123456789',
'shop_name' => '测试店铺', 'account_500112' => -20,
]],
'has_more' => true, 'status' => ['X-Caiwu-Data-Status' => 'direct'],
]);
$orderData->shouldReceive('afterSalesRows')->once()->with($filters, 2, 100)->andReturn([
'rows' => [[
'internal_order_no' => 'ORDER-101', 'account_500112' => null, 'reversal_order_amount' => 987.65,
]],
'has_more' => false, 'status' => ['X-Caiwu-Data-Status' => 'direct'],
]);
$this->app->instance(OrderDataPageData::class, $orderData);
$response = $this->loginAs()->get(route('business.bill-offset.export', $filters));
$response->assertOk()->assertHeader('Content-Type', 'text/csv; charset=UTF-8');
$csv = $response->streamedContent();
$this->assertStringContainsString('500112冲减收入', $csv);
$this->assertStringContainsString('测试店铺', $csv);
$this->assertStringContainsString('-20.00', $csv);
$this->assertStringContainsString("'001234567890123456789", $csv);
$this->assertStringContainsString('ORDER-1', $csv);
$this->assertStringContainsString('ORDER-101', $csv);
$this->assertStringNotContainsString('987.65', $csv);
$lines = preg_split('/\r?\n/', trim($csv));
$this->assertCount(3, $lines);
$this->assertSame('', str_getcsv($lines[2])[8]);
}
public function testFinancialAfterSalesPageRendersMirroredJushuitanRefunds(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$refundStates = [
NonJingxiRefundMirrorRepository::INFO_DATASET => [
'status' => 'succeeded',
'initial_completed_at' => '2026-08-24 12:00:00',
],
NonJingxiRefundMirrorRepository::ITEM_DATASET => [
'status' => 'succeeded',
'initial_completed_at' => '2026-08-24 12:00:00',
],
];
$refunds = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
$refunds->shouldReceive('available')->once()->andReturnTrue();
$refunds->shouldReceive('states')->once()->andReturn($refundStates);
$refunds->shouldReceive('syncStatus')->once()->with($refundStates)->andReturn([
'code' => 'healthy',
'last_succeeded_at' => '2026-08-24 12:00:00',
]);
$refunds->shouldReceive('ready')->once()->andReturnTrue();
$refunds->shouldReceive('queryReady')->once()->andReturnTrue();
$refunds->shouldReceive('paginate')->once()->withArgs(
static fn (array $filters, int $page, int $limit): bool =>
$filters === [
'from' => '2026-08-24',
'to' => '2026-08-24',
'order_no' => '',
'shop_name' => '天猫旗舰店',
'sku_id' => '',
] && $page === 1 && $limit === 10
)->andReturn([
'data' => [(object) [
'refundDate' => '2026-08-24 10:00:00',
'outer_as_id' => 'AS-FIN-001',
'o_id' => 'ORDER-FIN-001',
'so_id' => 'SUB-FIN-001',
'type' => '退款',
'shop_name' => '天猫旗舰店',
'platform' => '天猫',
'i_id' => 'ITEM-FIN-001',
'sku_id' => 'SKU-FIN-001',
'qty' => 1,
'detailRefundMoney' => 99.5,
'refundType' => '退货退款',
'RefundMoneyIncome' => 99.5,
'AfSalesMoney' => 99.5,
'status' => '完成',
]],
'total' => 1,
]);
$this->app->instance(NonJingxiRefundMirrorRepository::class, $refunds);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'financial-operations',
'page' => 'after-sales-expenses',
'from' => '2026-08-24',
'to' => '2026-08-24',
'shop_name' => '天猫旗舰店',
]))
->assertOk()
->assertDontSeeText('聚水潭售后工单')
->assertDontSeeText('WO-FIN-005')
->assertSeeText('售后(退货退款)明细')
->assertSeeText('AS-FIN-001')
->assertSeeText('ORDER-FIN-001')
->assertSeeText('SKU-FIN-001')
->assertSeeText('99.50')
->assertSeeText('退款回款金额');
}
public function testRefundPagesUseTheGenericBusinessPageRoute()
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$repository = \Mockery::mock(NonJingxiRefundMirrorRepository::class);
$repository->shouldReceive('available')->once()->andReturnFalse();
$repository->shouldNotReceive('states');
$repository->shouldNotReceive('syncStatus');
$repository->shouldNotReceive('ready');
$repository->shouldNotReceive('queryReady');
$repository->shouldNotReceive('paginate');
$this->app->instance(NonJingxiRefundMirrorRepository::class, $repository);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'refunds',
]))
->assertRedirect(route('business.pages.show', [
'section' => 'refunds',
'page' => 'jingxi-refund-data',
]));
$this->get(route('business.pages.show', [
'section' => 'refunds',
'page' => 'jingxi-refund-data',
]))
->assertOk()
->assertSeeText('京喜退款数据')
->assertSee('aria-label="京喜退款数据"', false)
->assertSee('data-header-row="labels"', false)
->assertSeeInOrder([
'店铺', '账单项目', '线上单号', '单据类型', '商品编码', '商品名称',
'费用计算时间', '费用发生时间', '费用项', '收入金额', '支出金额', '币种',
'钱包余额备注', '备注',
], false)
->assertSeeText('当前暂无京喜退款数据。')
->assertDontSee('class="biz-empty"', false);
$response = $this->get(route('business.pages.show', [
'section' => 'refunds',
'page' => 'non-jingxi-refund-data',
]));
$response
->assertOk()
->assertSeeText('售后(退货退款)')
->assertSee('aria-label="售后(退货退款)"', false)
->assertSee('aria-label="售后(退货退款)分页"', false)
->assertDontSeeText('非京喜退款数据')
->assertSeeText('共享业务镜像尚未建立')
->assertSeeText('数据暂时无法读取')
->assertSee('class="upstream-data-table refund-data-table refund-data-table--wide"', false)
->assertSee('data-header-row="labels"', false)
->assertSee('data-header-row="fields"', false)
->assertDontSee('class="biz-empty"', false);
$html = $response->getContent();
$headerRows = [];
preg_match_all(
'/<tr[^>]*data-header-row="(labels|fields)"[^>]*>(.*?)<\/tr>/s',
$html,
$headerRows,
PREG_SET_ORDER,
);
$headers = collect($headerRows)->mapWithKeys(function (array $row): array {
$cells = [];
preg_match_all('/<th[^>]*>(.*?)<\/th>/s', $row[2], $cells);
return [$row[1] => collect($cells[1])->map(
fn (string $cell): string => trim(html_entity_decode(strip_tags($cell)))
)->all()];
});
$this->assertSame([
'序号/主键ID', '退款日期', '售后单ID', '外部售后单号', '订单ID', '',
'子订单ID', '类型', '节点', '是否自营', '店铺名称', '平台', '责任部门',
'退款总金额', '原始邮编', '店铺商品ID', '店铺SKU编码', '商品ID', 'SKU编码',
'买家实付金额', '数量', '商品单位成本', '明细退款金额', '退款类型',
'退款数量', '退款商品成本', '退款回款金额', '售后金额', '创建时间', '状态',
'店铺状态', '创建记录/创建时间', '确认日期',
], $headers->get('labels'));
$this->assertSame([
'Id', 'refundDate', 'as_id', 'outer_as_id', 'o_id', '', 'so_id', 'type',
'node', 'isZC', 'shop_name', 'platform', 'responsibleDepartment',
'totalRefundMoney', 'originPostcode', 'shop_i_id', 'shop_sku_id', 'i_id',
'sku_id', 'buyer_paid_amount', 'qty', 'unitCostGoods', 'detailRefundMoney',
'refundType', 'refundQty', 'refundCostGoods', 'RefundMoneyIncome',
'AfSalesMoney', 'createTime', 'status', 'shop_status', 'created', 'confirm_date',
], $headers->get('fields'));
$this->withExceptionHandling()
->loginAs()
->withHeader('X-Requested-With', 'XMLHttpRequest')
->get(route('business.pages.show', [
'company_id' => $this->company->id,
'section' => 'refunds',
'page' => 'unknown-refund-page',
]))
->assertNotFound();
}
public function testPaperMaterialPagesRenderAsSeparateTables()
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$pages = [
'paper-prices' => '价格类型',
'paper-details' => '压贴厂金额',
'penalty-details' => '违约金',
'paper-factory-payables' => '2026年公司应付纸厂6月份货款',
];
foreach ($pages as $page => $expectedHeader) {
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'paper-materials',
'page' => $page,
]))
->assertOk()
->assertSee($expectedHeader)
->assertSee('class="paper-material-table', false)
->assertDontSee('class="business-blank-page"', false);
}
}
public function testEdgeBandingMaterialPagesRenderAsSeparateTables()
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$pages = [
'edge-banding-prices' => '价格类型',
'details' => '封边条厂单价',
'compensation' => '最新出库日期',
'edge-banding-payables' => '2026年公司应付封边条厂6月份货款',
];
foreach ($pages as $page => $expectedHeader) {
$response = $this->loginAs()
->get(route('business.pages.show', [
'section' => 'edge-banding-materials',
'page' => $page,
]))
->assertOk()
->assertSee($expectedHeader)
->assertSee('class="edge-banding-table', false)
->assertDontSee('class="business-blank-page"', false);
if ($page !== 'compensation') {
$response->assertSeeText('该上游数据同步尚未启用,请联系管理员。');
}
}
}
public function testLaminationFactoryPayablePagesRenderAsSeparateTables()
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$pages = [
'lamination-prices' => '执行日期',
'finished-board-prices' => '最新价格',
'lamination-fees' => '单号类型',
'finished-board-freight' => '成品板规格',
'paper-stock-in' => '家具厂',
'paper-stock-out' => '压贴厂金额',
'factory-raw-boards' => '纸张合计',
'raw-board-freight' => '素板规格',
'invoice-tax-points' => '对账金额',
'factory-compensation' => '来源金额',
'factory-payables-details' => '成品板运费合计',
'factory-payables-report' => '2026年公司应付压贴厂5月份货款',
];
foreach ($pages as $page => $expectedHeader) {
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'lamination-factory-payables',
'page' => $page,
]))
->assertOk()
->assertSee($expectedHeader)
->assertSee('class="lamination-table', false)
->assertDontSee('class="business-blank-page"', false);
}
}
public function testMergedFeaturePagesRenderTheirDedicatedContent()
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$pages = [
['supplier-payables', 'product-prices', '条形码'],
['courier-payables', 'freight-reconciliation', '快递单号'],
['courier-payables', 'bookkeeping-details', '暂无记账明细'],
['courier-payables', 'compensation-details', '暂无赔偿明细'],
['courier-payables', 'advance-details', '暂无借支明细'],
['courier-payables', 'other-adjustments', '暂无其他扣款补款明细'],
['raw-board-payables', 'board-prices', '价格类型'],
['raw-board-payables', 'board-reconciliation', '运费(包含分摊运费)'],
['raw-board-payables', 'compensation-details', '暂无素板赔付明细'],
['raw-board-payables', 'board-payables-report', '暂无素板应付汇总数据'],
];
foreach ($pages as [$section, $page, $expectedText]) {
$this->loginAs()
->get(route('business.pages.show', compact('section', 'page')))
->assertOk()
->assertSeeText($expectedText)
->assertDontSee('class="business-blank-page"', false);
}
}
public function testBoardInventoryDetailsRendersWorkbookColumnsAsItsOwnPage()
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'inventory-management',
'page' => 'board-inventory-details',
]))
->assertOk()
->assertSee('class="upstream-data-table upstream-data-table--inventory"', false)
->assertSeeText('加权平均单价')
->assertSeeText('上月剩余价值')
->assertSeeText('出库金额')
->assertDontSee('class="business-data-page"', false)
->assertDontSee('class="business-blank-page"', false);
}
public function testInventoryWarehouseMappingCanBeAddedEditedAndExportedAsExcel(): void
{
$provider = '测试仓储方-' . uniqid();
$warehouse = '测试仓库-' . uniqid();
$this->loginAs()->post(route('business.product-inventory.inventory-report.warehouse-mappings.store'), [
'warehouse_provider' => $provider,
'warehouse' => $warehouse,
])->assertRedirect();
$mapping = InventoryWarehouseNameMapping::query()
->where('warehouse_provider', $provider)
->where('warehouse', $warehouse)
->firstOrFail();
$this->assertStringStartsWith('测试仓库', $mapping->warehouse_short_name);
$this->loginAs()->patch(route('business.product-inventory.inventory-report.warehouse-mappings.update', $mapping->id), [
'warehouse_provider' => $provider,
'warehouse' => $warehouse,
'warehouse_short_name' => '测试简称',
])->assertRedirect();
$response = $this->loginAs()->get(route('business.product-inventory.inventory-report.warehouse-mappings.export'));
$response->assertOk()
->assertDownload()
->assertHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
$path = tempnam(sys_get_temp_dir(), 'inventory-mapping-export-');
file_put_contents($path, $response->streamedContent());
try {
$sheet = IOFactory::load($path)->getActiveSheet();
$this->assertSame(['仓储方', '仓库', '仓库简称'], $sheet->rangeToArray('A1:C1')[0]);
$this->assertSame([$provider, $warehouse, '测试简称'], $sheet->rangeToArray('A2:C2')[0]);
} finally {
@unlink($path);
}
}
public function testInventoryWarehouseMappingPageUsesTheIndependentLedger(): void
{
$this->loginAs()->get(route('business.pages.show', [
'section' => 'product-inventory',
'page' => 'inventory-report',
'mapping' => 1,
]))->assertOk()
->assertSeeText('仓储方')
->assertSeeText('仓库简称')
->assertSeeText('导入并预览')
->assertSeeText('导出完整表格(Excel)')
->assertSeeText('选择文件')
->assertSee('class="inventory-warehouse-mapping__export"', false)
->assertSee('class="inventory-warehouse-mapping__file-label"', false)
->assertSee('class="inventory-warehouse-mapping__import-button"', false)
->assertSee('data-inventory-mapping-create', false)
->assertSee('data-inventory-mapping-edit', false)
->assertSee('data-directory-table-key="warehouse-mapping"', false)
->assertSee('data-inventory-mapping-template', false)
->assertSeeText('当前页·显示列')
->assertSeeText('全部筛选结果·全部字段')
->assertSee('.inventory-warehouse-mapping__table thead,', false)
->assertSee('position: sticky;', false)
->assertSee('data-directory-fixed="start"', false)
->assertSee('data-directory-fixed="end"', false);
}
}