CaiWuXiTong/tests/Feature/Business/CourierSurchargePageTest.php
zhoushengchen f40c102a7b 新增快递附加费操作日志 (#227)
## 实际改动
- 为快递附加费报价规则和附加费类型记录新增、更改、启用、停用日志。
- 日志按公司隔离,记录操作人、时间、对象及安全字段的变更前后值,页面展示最近 50 条。
- 报价/类型写入与日志记录使用同一事务;审计表未迁移时保留只读查看并阻止无日志写入。

## 修改范围
- 新增审计日志 Repository、Model 和迁移。
- 修改报价/类型 Repository、页面控制器、现有 VIP 黑金页面和对应 Feature 测试。
- 不修改路由、权限、菜单、上游数据源或生产配置。

## 协作检查
- 已获取最新 `main` 并检查远程任务分支。
- `origin/feature/limeng-shipping-data-filters` 同样修改 `Pages.php`,但其逻辑为独立的发货数据页面,与本任务方法、路由、Repository、表和视图无实际重叠。

## 验证
- `CourierSurchargePageTest`:9 个测试通过。
- `CourierPayableSummaryPagesTest`:14 个测试通过。
- 合计 23 个测试、336 个断言通过。
- 已验证现有报价可见、未迁移警告及写入禁用状态。

## 风险与授权
- 新增 1 张审计日志表,需要随生产发布执行迁移。
- 用户已明确要求主线部署,授权本次迁移与发布。
- 未提交 `.env`、密码、Token、私钥、真实财务数据、生产日志或构建产物。
- 回滚代码时保留审计表及历史记录,不执行破坏性删表。

## 合并前自审
- 已逐文件审核完整 diff,确认无越界修改、重复实现、敏感内容和未处理冲突。
- 使用 Squash merge 合并到最新 `main`,再按标准脚本部署。

Reviewed-on: http://git.1080p.hk/WuYongJie/CaiWuXiTong/pulls/227
2026-09-21 10:42:13 +08:00

379 lines
17 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\Business\CourierSurchargeTypeRepository;
use App\Models\Auth\Permission;
use App\Models\Business\CourierSurchargeAuditLog;
use App\Models\Business\CourierSurchargeQuote;
use App\Models\Business\CourierSurchargeType;
use Illuminate\Support\Facades\Schema;
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('附加费类型(可多选)')
->assertSeeText('列设置')
->assertSeeText('导出当前列')
->assertSeeText('导出全部字段')
->assertSeeText('规则编号与操作列固定显示')
->assertSee('data-column-storage-key="caiwu:table-columns:', false)
->assertSee('data-surcharge-table', false)
->assertSee('data-column-check', false)
->assertSee('data-surcharge-export-current', false)
->assertSee('data-surcharge-export-all', false)
->assertSee('data-column-reset', false)
->assertSee('name="types[]"', false)
->assertSeeInOrder(['data-column-key="courier"', 'data-column-key="scope"', 'data-column-key="reconciler"'], 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);
}
public function test_quote_operations_are_recorded_with_actor_and_changed_fields(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$payload = [
'courier' => '日志测试快递',
'scope' => '自有快递',
'reconciler' => '周会计',
'types' => ['进仓费'],
'mode' => '固定单价',
'standard' => '10 元/单',
'condition' => '完成进仓',
'effective_at' => '2026-09-21',
'remark' => '初始标准',
];
$this->post(route('business.courier-surcharge.store'), $payload)->assertRedirect();
$quote = CourierSurchargeQuote::query()->where('courier', '日志测试快递')->firstOrFail();
$created = CourierSurchargeAuditLog::query()
->where('target_type', CourierSurchargeAuditLog::TARGET_QUOTE)
->where('target_id', $quote->id)
->where('action', CourierSurchargeAuditLog::ACTION_CREATED)
->firstOrFail();
$this->assertSame((int) user_id(), (int) $created->actor_id);
$this->assertSame('日志测试快递 · 进仓费', $created->target_label);
$this->assertSame('10 元/单', data_get($created->changes, 'after.standard'));
$this->patch(route('business.courier-surcharge.update', ['quote' => $quote->id]), [
...$payload,
'standard' => '12 元/单',
'remark' => '调整后标准',
])->assertRedirect();
$updated = CourierSurchargeAuditLog::query()
->where('target_type', CourierSurchargeAuditLog::TARGET_QUOTE)
->where('target_id', $quote->id)
->where('action', CourierSurchargeAuditLog::ACTION_UPDATED)
->firstOrFail();
$this->assertSame('10 元/单', data_get($updated->changes, 'before.standard'));
$this->assertSame('12 元/单', data_get($updated->changes, 'after.standard'));
$this->assertSame('初始标准', data_get($updated->changes, 'before.remark'));
$this->assertSame('调整后标准', data_get($updated->changes, 'after.remark'));
$this->post(route('business.courier-surcharge.status', ['quote' => $quote->id]), [
'status' => CourierSurchargeQuote::STATUS_INACTIVE,
])->assertRedirect();
$this->post(route('business.courier-surcharge.status', ['quote' => $quote->id]), [
'status' => CourierSurchargeQuote::STATUS_ACTIVE,
])->assertRedirect();
$this->assertDatabaseHas('business_courier_surcharge_audit_logs', [
'company_id' => company_id(),
'target_type' => CourierSurchargeAuditLog::TARGET_QUOTE,
'target_id' => $quote->id,
'action' => CourierSurchargeAuditLog::ACTION_DEACTIVATED,
]);
$this->assertDatabaseHas('business_courier_surcharge_audit_logs', [
'company_id' => company_id(),
'target_type' => CourierSurchargeAuditLog::TARGET_QUOTE,
'target_id' => $quote->id,
'action' => CourierSurchargeAuditLog::ACTION_ACTIVATED,
]);
$this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->assertOk()
->assertSeeText('操作日志')
->assertSeeText('新增')
->assertSeeText('更改')
->assertSeeText('停用')
->assertSeeText('启用')
->assertSeeText($this->user->name)
->assertSeeText('计价标准:10 元/单 → 12 元/单');
}
public function test_type_operations_are_recorded_and_other_company_logs_are_hidden(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$this->loginAs();
$this->post(route('business.courier-surcharge.types.store'), ['name' => '日志专用类型'])
->assertRedirect();
$type = CourierSurchargeType::query()->where('name', '日志专用类型')->firstOrFail();
$this->post(route('business.courier-surcharge.types.status', ['type' => $type->id]), [
'status' => CourierSurchargeType::STATUS_INACTIVE,
])->assertRedirect();
$this->post(route('business.courier-surcharge.types.status', ['type' => $type->id]), [
'status' => CourierSurchargeType::STATUS_ACTIVE,
])->assertRedirect();
$this->assertSame([
CourierSurchargeAuditLog::ACTION_CREATED,
CourierSurchargeAuditLog::ACTION_DEACTIVATED,
CourierSurchargeAuditLog::ACTION_ACTIVATED,
], CourierSurchargeAuditLog::query()
->where('target_type', CourierSurchargeAuditLog::TARGET_TYPE)
->where('target_id', $type->id)
->orderBy('id')
->pluck('action')
->all());
CourierSurchargeAuditLog::withoutGlobalScopes()->create([
'company_id' => company_id() + 999,
'actor_id' => user_id(),
'action' => CourierSurchargeAuditLog::ACTION_CREATED,
'target_type' => CourierSurchargeAuditLog::TARGET_TYPE,
'target_id' => 999,
'target_label' => '其他公司的私有类型',
'changes' => ['after' => ['name' => '其他公司的私有类型']],
]);
$this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->assertOk()
->assertSeeText('附加费类型 · 日志专用类型')
->assertDontSeeText('其他公司的私有类型');
}
public function test_existing_quotes_remain_visible_but_writes_stop_when_audit_table_is_missing(): void
{
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
$quote = $this->createQuote();
Schema::drop('business_courier_surcharge_audit_logs');
$this->loginAs()
->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
->assertOk()
->assertSeeText($quote->courier)
->assertSeeText('操作日志表尚未完成迁移')
->assertSee('data-surcharge-open disabled', false);
$this->withExceptionHandling()->post(route('business.courier-surcharge.store'), [
'courier' => '不应保存的快递',
'scope' => '自有快递',
'reconciler' => '周会计',
'types' => ['进仓费'],
'mode' => '固定单价',
'standard' => '10 元/单',
'condition' => '测试',
'effective_at' => '2026-09-21',
])->assertServiceUnavailable();
$this->assertDatabaseMissing('business_courier_surcharge_quotes', ['courier' => '不应保存的快递']);
}
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' => '大件渠道标准',
]);
}
}