CaiWuXiTong/tests/Feature/Business/CourierSurchargePageTest.php
zhoushengchen 88526b9619 附加费类型字典自维护(含进仓费) (#98)
## 任务占用与边界
- 任务名称:附加费类型字典自维护
- 实际开发者:zhoushengchen
- 目标结果:在快递附加费报价维护页支持附加费类型的新增、启用/停用;新增报价仅可选择启用类型,历史报价保留原类型。
- 预计修改路径:附加费类型 Repository、Model、Controller、报价 Repository/Controller、页面、管理路由、类型表迁移和专项测试。
- 将写入的公共契约:business.courier-surcharge.types.store、business.courier-surcharge.types.status、business_courier_surcharge_types。
- 仅复用、不修改的公共契约:business.pages.show、business.courier-surcharge.store。
- 明确不修改的范围:上游数据库、业务镜像、七牛、生产配置和无关页面。

## 开工碰撞检查
- [x] 已检查最新 main 和所有远程任务分支
- [x] 已运行 scripts/codex-task-preflight.ps1
- [x] 未发现与其他远程任务的实际写入重叠

## 实际改动
- 新增附加费类型字典及公司范围隔离。
- 新增类型管理弹窗、创建接口、启用/停用接口。
- 报价表单和后端校验动态读取启用类型;筛选仍可查看全部类型。
- 默认初始化原有 11 种类型,并增加“进仓费”。
- 增加幂等增量迁移,补齐已执行建表迁移环境中的“进仓费”。

## 验证证据
- tests/Feature/Business/CourierSurchargePageTest.php:6 个用例、60 个断言通过。
- 相关 PHP 文件语法检查通过。
- business.courier-surcharge 路由共 5 条,路由检查通过。
- git diff --check 通过。
- 本地共享系统库现状:类型表已存在,当前公司有 11 个旧类型;增量迁移待执行。

## 风险检查
- 未提交 .env、密码、Token、私钥、真实财务数据、生产日志或构建产物。
- 前端遵循 DESIGN.md 的 VIP 黑金令牌。
- 数据库影响:仅新增类型表(若环境尚未建表)或幂等补充“进仓费”,不删除、不覆盖已有类型。
- 回滚:类型字典不做物理删除;迁移 down 保持空操作以保护业务引用。
- 已获得本次“合并 PR 并执行类型表迁移”的明确授权;迁移目标为本机当前连接的团队系统数据库,不涉及生产部署。

## 合并前复查
- [x] 已重新 fetch 远程引用并再次运行碰撞检查
- [x] 已逐文件审核 origin/main...任务分支完整 diff
- [ ] PR 创建后移除 WIP 并执行 Squash merge
- [ ] 合并后验证 main、执行迁移并验证页面

Reviewed-on: http://git.1080p.hk/WuYongJie/CaiWuXiTong/pulls/98
2026-09-02 18:22:17 +08:00

221 lines
9.1 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\CourierSurchargeType;
use App\Models\Business\CourierSurchargeQuote;
use App\Business\CourierSurchargeTypeRepository;
use Tests\Feature\FeatureTestCase;
class CourierSurchargePageTest extends FeatureTestCase
{
public function test_courier_surcharge_page_renders_persisted_quote_rules(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->createQuote();
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'courier-surcharge',
]))
->assertOk()
->assertSeeText('附加费报价维护')
->assertSeeText('正式维护数据')
->assertSeeText('报价规则列表')
->assertSeeText('管理附加费类型')
->assertSeeText('类型列表')
->assertSeeText('进仓费')
->assertSeeText('新增报价规则')
->assertSeeText('结算方式')
->assertSeeText('计价标准')
->assertSeeText('生效条件')
->assertSeeText('快递归属')
->assertSeeText('自有快递')
->assertSeeText('代发仓')
->assertSeeText('对账人')
->assertSeeText('张会计')
->assertSeeText('附加费类型(可多选)')
->assertSee('name="types[]"', false)
->assertSeeInOrder(['<th>快递名称</th>', '<th>快递归属</th>', '<th>对账人</th>'], false)
->assertSeeText('曹县德邦')
->assertSeeText('超长费')
->assertSeeText('按尺寸/最长边')
->assertSeeText('8 元/单')
->assertSeeText('已启用')
->assertSeeText('后续结算页面将按启用中的规则计算');
}
public function test_courier_surcharge_type_can_be_created_and_duplicate_is_rejected(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$this->post(route('business.courier-surcharge.types.store'), ['name' => '偏远地区费'])
->assertRedirect();
$this->assertDatabaseHas('business_courier_surcharge_types', [
'company_id' => company_id(),
'name' => '偏远地区费',
'status' => CourierSurchargeType::STATUS_ACTIVE,
]);
$this->withExceptionHandling()
->from(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->post(route('business.courier-surcharge.types.store'), ['name' => '偏远地区费'])
->assertRedirect()
->assertSessionHasErrors('name');
}
public function test_inactive_type_is_rejected_for_new_quotes_but_keeps_historical_value(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$type = app(CourierSurchargeTypeRepository::class)->all()->firstWhere('name', '超长费');
$this->assertNotNull($type);
$quote = $this->createQuote();
$this->post(route('business.courier-surcharge.types.status', ['type' => $type->id]), [
'status' => CourierSurchargeType::STATUS_INACTIVE,
])->assertRedirect();
$this->withExceptionHandling()->post(route('business.courier-surcharge.store'), [
'courier' => '停用类型测试快递',
'scope' => '自有快递',
'reconciler' => '张会计',
'types' => ['超长费'],
'mode' => '固定单价',
'standard' => '1 元/单',
'condition' => '测试',
'effective_at' => '2026-09-02',
])->assertSessionHasErrors('types.0');
$this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->assertOk()
->assertSeeText($quote->courier)
->assertSeeText('超长费')
->assertSeeText('管理附加费类型');
$page = $this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->assertOk();
$this->assertStringNotContainsString('value="超长费"><span>超长费</span>', $page->getContent());
}
public function test_type_status_endpoint_rejects_another_company_type(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$otherType = CourierSurchargeType::withoutGlobalScopes()->create([
'company_id' => company_id() + 999,
'name' => '其他公司类型',
'status' => CourierSurchargeType::STATUS_ACTIVE,
'sort_order' => 99,
]);
$this->loginAs();
$this->withExceptionHandling()->post(route('business.courier-surcharge.types.status', ['type' => $otherType->id]), [
'status' => CourierSurchargeType::STATUS_INACTIVE,
])->assertNotFound();
}
public function test_courier_surcharge_page_filters_persisted_rules(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->createQuote();
CourierSurchargeQuote::query()->create([
'company_id' => company_id(),
'courier' => '常州松宇货运',
'scope' => '代发仓',
'reconciler' => '李会计',
'types' => ['送货费'],
'mode' => '按体积',
'standard' => '体积 ≤ 10 m³:50 元/单',
'condition' => '计费体积不超过 10 m³',
'effective_at' => '2026-09-02',
'status' => CourierSurchargeQuote::STATUS_ACTIVE,
'remark' => null,
]);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'courier-surcharge',
'courier' => '常州松宇货运',
'scope' => '代发仓',
'reconciler' => '李会计',
'status' => '已启用',
]))
->assertOk()
->assertSeeText('常州松宇货运')
->assertSeeText('送货费')
->assertSeeText('代发仓')
->assertSeeText('李会计')
->assertSeeText('当前规则')
->assertDontSee('<td>曹县德邦</td>', false)
->assertDontSee('<td>霸州百世快运-雅柏</td>', false);
}
public function test_courier_surcharge_store_update_and_status_are_persistent(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$payload = [
'courier' => '测试快递',
'scope' => '代发仓',
'reconciler' => '李会计',
'types' => ['拦截费', '改地址'],
'mode' => '固定单价',
'standard' => '12 元/单',
'condition' => '拦截成功',
'effective_at' => '2026-09-02',
'remark' => '接口保存测试',
];
$this->post(route('business.courier-surcharge.store'), $payload)
->assertRedirect(route('business.pages.show', [
'company_id' => company_id(), 'section' => 'courier-payables', 'page' => 'courier-surcharge',
]));
$quote = CourierSurchargeQuote::query()->where('courier', '测试快递')->firstOrFail();
$this->assertSame(['拦截费', '改地址'], $quote->types);
$this->assertSame(CourierSurchargeQuote::STATUS_ACTIVE, $quote->status);
$this->assertSame((int) user_id(), (int) $quote->created_by);
$this->assertSame((int) user_id(), (int) $quote->updated_by);
$this->patch(route('business.courier-surcharge.update', ['quote' => $quote->id]), [
...$payload,
'courier' => '测试快递(已修改)',
'types' => ['保费'],
])->assertRedirect();
$quote->refresh();
$this->assertSame('测试快递(已修改)', $quote->courier);
$this->assertSame(['保费'], $quote->types);
$this->post(route('business.courier-surcharge.status', ['quote' => $quote->id]), [
'status' => CourierSurchargeQuote::STATUS_INACTIVE,
])->assertRedirect();
$this->assertSame(CourierSurchargeQuote::STATUS_INACTIVE, $quote->fresh()->status);
}
private function createQuote(): CourierSurchargeQuote
{
return CourierSurchargeQuote::query()->create([
'company_id' => company_id(),
'courier' => '曹县德邦',
'scope' => '自有快递',
'reconciler' => '张会计',
'types' => ['超长费'],
'mode' => '按尺寸/最长边',
'standard' => '最长边 > 160 cm:8 元/单',
'condition' => '最长边超过 160 cm',
'effective_at' => '2026-09-01',
'status' => CourierSurchargeQuote::STATUS_ACTIVE,
'remark' => '大件渠道标准',
]);
}
}