Files
xin-procurement/tests/Feature/ContainerReturnTest.php
T

318 lines
13 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;
use App\Exports\ContainerReturnExport;
use App\Models\BillModel;
use App\Models\ContainerReturnModel;
use App\Models\CustomerLevelModel;
use App\Models\ProductModel;
use App\Models\PurchaseOrderModel;
use App\Models\StoreModel;
use App\Models\StoreOrderModel;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
use Modules\SystemTool\Services\SysSiteConfigService;
/**
* 压回筐记录:周转筐/托盘跟账单走(生成账单时写入完整快照,只读);
* 数量正数=压筐(附加金额),负数=回筐(抵扣金额),数量为 0 不写记录;
* 门店表不再记录待回数量
*/
class ContainerReturnTest extends ProcurementTestCase
{
/** 注入站点配置:筐单价 5.00、托盘单价 20.00 */
private function seedContainerConfig(): void
{
DB::table('sys_site_config_group')->insert([
'id' => 100,
'title' => '业务配置',
'key' => 'services',
'remark' => '',
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('sys_site_config_items')->insert([
[
'group_id' => 100, 'key' => 'box_amount', 'title' => '周转筐金额', 'describe' => '',
'values' => '5.00', 'type' => 'InputNumber', 'options' => null, 'props' => null, 'sort' => 0,
'created_at' => now(), 'updated_at' => now(),
],
[
'group_id' => 100, 'key' => 'tray_amount', 'title' => '周转托盘金额', 'describe' => '',
'values' => '20.00', 'type' => 'InputNumber', 'options' => null, 'props' => null, 'sort' => 1,
'created_at' => now(), 'updated_at' => now(),
],
]);
SysSiteConfigService::refreshSiteConfig();
}
/**
* 完整业务链造账单(商品 5.00 × 4 = 20.00,配送费 0),按指定筐/托盘数量生成
*
* @return array{0: StoreModel, 1: BillModel}
*/
private function makeBillWithContainers(int $boxNum, int $trayNum): array
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$product = ProductModel::factory()->create(['status' => ProductModel::STATUS_ON, 'cost_price' => '5.00']);
$this->actingAsMiniStore($store);
$this->postJson('/mini/order', ['items' => [['product_id' => $product->id, 'quantity' => 4]]])
->assertJsonPath('success', true);
$order = StoreOrderModel::where('store_id', $store->id)->first();
$this->actingAsSysUser();
$this->putJson("/order/store/{$order->id}/status", ['status' => StoreOrderModel::STATUS_SUMMARIZED])
->assertJsonPath('success', true);
$this->postJson('/purchase/order/generate', ['purchase_date' => now()->toDateString()])
->assertJsonPath('success', true);
$purchase = PurchaseOrderModel::first();
$this->putJson("/purchase/order/{$purchase->id}/finish")->assertJsonPath('success', true);
$this->postJson("/purchase/order/{$purchase->id}/bill", [
'stores' => [[
'store_id' => $store->id,
'delivery_fee' => 0,
'box_num' => $boxNum,
'tray_num' => $trayNum,
]],
])->assertJsonPath('success', true);
return [$store, BillModel::where('store_id', $store->id)->first()];
}
/** 压筐(正数):附加金额增加账单总额,写入完整快照记录 */
public function test_press_containers_adds_amount_and_writes_record(): void
{
$this->seedContainerConfig();
[$store, $bill] = $this->makeBillWithContainers(3, 2);
// 附加 = 3×5 + 2×20 = 55;总额 = 20 + 55 = 75
$this->assertSame('55.00', (string) $bill->added_amount);
$this->assertSame('75.00', (string) $bill->total_amount);
$record = ContainerReturnModel::where('bill_id', $bill->id)->first();
$this->assertNotNull($record);
$this->assertSame($store->id, $record->store_id);
$this->assertSame(3, $record->box_num);
$this->assertSame(2, $record->tray_num);
$this->assertSame('5.00', (string) $record->box_price, '筐单价快照');
$this->assertSame('20.00', (string) $record->tray_price, '托盘单价快照');
$this->assertSame('55.00', (string) $record->amount);
}
/** 回筐(负数):抵扣账单金额,记录为负 */
public function test_return_containers_deducts_amount(): void
{
$this->seedContainerConfig();
[, $bill] = $this->makeBillWithContainers(-2, 0);
// 附加 = -2×5 + 0 = -10;总额 = 20 - 10 = 10
$this->assertSame('-10.00', (string) $bill->added_amount);
$this->assertSame('10.00', (string) $bill->total_amount);
$record = ContainerReturnModel::where('bill_id', $bill->id)->first();
$this->assertNotNull($record);
$this->assertSame(-2, $record->box_num);
$this->assertSame(0, $record->tray_num);
$this->assertSame('-10.00', (string) $record->amount);
}
/** 混合:筐回(负)+ 托盘压(正),金额按代数和计 */
public function test_mixed_sign_containers(): void
{
$this->seedContainerConfig();
[, $bill] = $this->makeBillWithContainers(-3, 1);
// 附加 = -3×5 + 1×20 = 5;总额 = 20 + 5 = 25
$this->assertSame('5.00', (string) $bill->added_amount);
$this->assertSame('25.00', (string) $bill->total_amount);
$record = ContainerReturnModel::where('bill_id', $bill->id)->first();
$this->assertNotNull($record);
$this->assertSame(-3, $record->box_num);
$this->assertSame(1, $record->tray_num);
$this->assertSame('5.00', (string) $record->amount);
}
/** 数量为 0:不写压回筐记录 */
public function test_zero_containers_writes_no_record(): void
{
$this->seedContainerConfig();
[, $bill] = $this->makeBillWithContainers(0, 0);
$this->assertSame('20.00', (string) $bill->total_amount);
$this->assertSame(0, ContainerReturnModel::where('bill_id', $bill->id)->count());
}
/** 筐/托盘数量非整数 → 拒绝 */
public function test_non_integer_containers_rejected(): void
{
$level = CustomerLevelModel::factory()->create();
$store = StoreModel::factory()->create(['level_id' => $level->id]);
$this->actingAsSysUser();
$purchase = PurchaseOrderModel::factory()->create(['status' => PurchaseOrderModel::STATUS_COMPLETED]);
$this->postJson("/purchase/order/{$purchase->id}/bill", [
'stores' => [[
'store_id' => $store->id,
'delivery_fee' => 0,
'box_num' => 1.5,
'tray_num' => 0,
]],
])->assertJsonPath('success', false);
}
/** 压回筐记录只读:手动登记/删除端点已下线 */
public function test_manual_create_and_delete_routes_removed(): void
{
$this->actingAsSysUser();
$this->postJson('/recon/container-return', [
'store_id' => 1,
'box_num' => 1,
'tray_num' => 0,
])->assertJsonPath('success', false);
$this->deleteJson('/recon/container-return/1')->assertJsonPath('success', false);
}
/** 造一条压回筐记录(直接入库并指定记录时间,用于导出区间/合并断言) */
private function makeRecord(StoreModel $store, string $amount, string $createdAt): void
{
DB::table('container_return')->insert([
'store_id' => $store->id,
'bill_id' => 0,
'box_num' => 0,
'tray_num' => 0,
'box_price' => '5.00',
'tray_price' => '20.00',
'amount' => $amount,
'operator_id' => 0,
'created_at' => $createdAt,
'updated_at' => $createdAt,
]);
}
/** 汇总导出:行=日期(区间内逐日),列=门店,同日记录合并,无记录填 0,含行/列合计 */
public function test_export_matrix_merges_same_day_and_zero_fills(): void
{
$storeA = StoreModel::factory()->create();
$storeB = StoreModel::factory()->create();
// 08-01:A 两条同日记录合并(+55 压筐、-10 回筐 = 45),B 一条 -20
$this->makeRecord($storeA, '55.00', '2026-08-01 09:00:00');
$this->makeRecord($storeA, '-10.00', '2026-08-01 15:30:00');
$this->makeRecord($storeB, '-20.00', '2026-08-01 10:00:00');
// 08-02:仅 A +5B 无记录填 0
$this->makeRecord($storeA, '5.00', '2026-08-02 08:00:00');
// 区间外记录不参与
$this->makeRecord($storeB, '30.00', '2026-08-04 08:00:00');
$this->actingAsSysUser();
Excel::fake();
$this->get('/recon/container-return/export?start_date=2026-08-01&end_date=2026-08-03')->assertOk();
Excel::assertDownloaded(
'回筐记录_2026-08-01_2026-08-03.xlsx',
static function (ContainerReturnExport $export) use ($storeA, $storeB): bool {
$rows = $export->collection()->values();
// 标题1 + 范围1 + 空行1 + 列头1 + 日期3 + 合计1 = 8 行
if ($rows->count() !== 8) {
return false;
}
// 列头:日期 + 门店(未选门店时取区间内有记录的门店,ID 升序)+ 合计
if ($rows[3] !== ['日期', $storeA->name, $storeB->name, '合计']) {
return false;
}
// 08-01:同日合并 55-10=45B -20,行合计 25
if ($rows[4] !== ['2026-08-01', 45.0, -20.0, 25.0]) {
return false;
}
// 08-02A 5、B 无记录填 0,行合计 5
if ($rows[5] !== ['2026-08-02', 5.0, 0.0, 5.0]) {
return false;
}
// 08-03:当日全部无记录,整行填 0
if ($rows[6] !== ['2026-08-03', 0.0, 0.0, 0.0]) {
return false;
}
// 合计行:A 列 50、B 列 -20、总计 30
return $rows[7] === ['合计', 50.0, -20.0, 30.0];
}
);
}
/** 指定门店导出:仅所选门店成列,未选门店记录忽略;所选门店无记录整列填 0 */
public function test_export_filters_by_selected_stores(): void
{
$storeA = StoreModel::factory()->create();
$storeB = StoreModel::factory()->create();
$storeC = StoreModel::factory()->create();
$this->makeRecord($storeA, '55.00', '2026-08-01 09:00:00');
$this->makeRecord($storeB, '-20.00', '2026-08-01 10:00:00');
// C 区间内无记录,但被选中 → 整列填 0
$this->actingAsSysUser();
Excel::fake();
$this->get('/recon/container-return/export?start_date=2026-08-01&end_date=2026-08-02&store_ids=' . $storeA->id . ',' . $storeC->id)
->assertOk();
Excel::assertDownloaded(
'回筐记录_2026-08-01_2026-08-02.xlsx',
static function (ContainerReturnExport $export) use ($storeA, $storeC): bool {
$rows = $export->collection()->values();
// 列头仅含所选门店(B 的记录被忽略)
if ($rows[3] !== ['日期', $storeA->name, $storeC->name, '合计']) {
return false;
}
// 08-01A 55、C 填 008-02 全 0
if ($rows[4] !== ['2026-08-01', 55.0, 0.0, 55.0]) {
return false;
}
if ($rows[5] !== ['2026-08-02', 0.0, 0.0, 0.0]) {
return false;
}
return $rows[6] === ['合计', 55.0, 0.0, 55.0];
}
);
}
/** 参数校验:缺少日期 / 结束早于开始 / 区间超 366 天 → 拒绝 */
public function test_export_invalid_params_rejected(): void
{
$this->actingAsSysUser();
$this->get('/recon/container-return/export')
->assertJsonPath('success', false);
$this->get('/recon/container-return/export?start_date=2026-08-10&end_date=2026-08-01')
->assertJsonPath('success', false);
$this->get('/recon/container-return/export?start_date=2026-01-01&end_date=2027-06-30')
->assertJsonPath('success', false);
}
/** 区间内无记录且未选门店 → 拒绝 */
public function test_export_empty_range_rejected(): void
{
$this->actingAsSysUser();
$this->get('/recon/container-return/export?start_date=2026-08-01&end_date=2026-08-03')
->assertJsonPath('success', false);
}
/** 无 recon.containerReturn.export 权限点 → 拦截 */
public function test_export_requires_export_permission(): void
{
// 先建一个占位用户:每个测试方法内首个系统用户自增 id=1,
// SysAccessToken::can() 对 tokenable_id==1(超管)放行全部权限,会绕过 abilities 校验
$this->actingAsSysUser();
// 仅持有查询权限的用户
$this->actingAsSysUser(['recon.containerReturn.query']);
$response = $this->get('/recon/container-return/export?start_date=2026-08-01&end_date=2026-08-03');
$this->assertFalse($response->json('success'), '缺少权限点应被拦截');
}
}