1408 lines
64 KiB
PHP
1408 lines
64 KiB
PHP
<?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&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);
|
||
}
|
||
}
|