201 lines
13 KiB
PHP
201 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Business;
|
|
|
|
use App\Models\Auth\DingTalkIdentity;
|
|
use App\Models\Auth\Permission;
|
|
use App\Models\Auth\Role;
|
|
use App\Models\Auth\User;
|
|
use App\Models\Business\CourierOnboarding;
|
|
use App\Models\Business\CourierQuoteChange;
|
|
use App\Models\Common\Company;
|
|
use App\Services\Auth\FinanceRoster;
|
|
use App\Support\CourierQuoteWorkflowPermissions;
|
|
use App\Support\CourierOnboardingService;
|
|
use App\Support\QuoteManagementSnapshot;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Tests\Feature\FeatureTestCase;
|
|
|
|
class CourierQuoteWorkflowPermissionsTest extends FeatureTestCase
|
|
{
|
|
private string $fixturePath;
|
|
private array $people;
|
|
private bool $eligible = true;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->mock(FinanceRoster::class, function ($mock): void {
|
|
$mock->shouldReceive('findEligible')->andReturnUsing(fn ($id) => $this->eligible ? ['display_name' => $id, 'restricted' => false] : null);
|
|
$mock->shouldReceive('search')->andReturn([['flowerNameId' => 'review-one', 'name' => '审核一', 'flowerName' => '审核一']]);
|
|
});
|
|
$this->people = [];
|
|
foreach (['init-one', 'init-two', 'review-one', 'review-two'] as $id) {
|
|
$person = User::create(['name' => $id, 'email' => $id . '@example.test', 'password' => 'password',
|
|
'locale' => 'zh-CN', 'enabled' => true, 'landing_page' => 'dashboard', 'created_from' => 'core::test']);
|
|
$person->companies()->attach($this->company->id);
|
|
$person->roles()->attach(Role::where('name', 'user')->firstOrFail()->id);
|
|
$person->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
|
DingTalkIdentity::create(['user_id' => $person->id, 'union_id' => 'union-' . $id, 'dingtalk_user_id' => $id, 'bound_at' => now()]);
|
|
$this->people[$id] = $person;
|
|
}
|
|
$this->fixturePath = storage_path('framework/testing/quote-permissions-' . uniqid() . '.json');
|
|
file_put_contents($this->fixturePath, json_encode(['sourcePath' => 'private/quote.xlsx', 'sheets' => [[
|
|
'index' => 0, 'name' => '测试快递报价', 'address' => 'A1:B2',
|
|
'values' => [['省份', '面单'], ['江苏', 5]], 'formulas' => [[], []],
|
|
]]], JSON_UNESCAPED_UNICODE));
|
|
app()->instance(QuoteManagementSnapshot::class, new QuoteManagementSnapshot($this->fixturePath));
|
|
$this->loginAs();
|
|
app(CourierQuoteWorkflowPermissions::class)->save($this->permissionGroups(), $this->user->name, $this->user->id);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
@unlink($this->fixturePath);
|
|
parent::tearDown();
|
|
}
|
|
|
|
private function permissionGroups(): array
|
|
{
|
|
return ['initiators' => ['init-one', 'init-two'], 'reviewers' => ['review-one', 'review-two']];
|
|
}
|
|
|
|
private function payload(): array
|
|
{
|
|
return ['source_quote_id' => 'QB-001', 'change_type' => '费率调整', 'effective_date' => now()->addDay()->toDateString(),
|
|
'summary' => '调整江苏面单价格', 'items' => [['source_cell' => 'B2', 'source_row' => 1, 'source_column' => 1,
|
|
'region' => '江苏', 'weight_band' => '面单', 'charge_item' => '面单', 'original_amount' => '5', 'new_amount' => '6', 'unit' => '元']]];
|
|
}
|
|
|
|
private function pending(string $initiator = 'init-one'): CourierQuoteChange
|
|
{
|
|
$this->loginAs($this->people[$initiator], $this->company)
|
|
->post(route('business.courier-quote-changes.store'), $this->payload())->assertRedirect();
|
|
$change = CourierQuoteChange::latest('id')->firstOrFail();
|
|
$this->post(route('business.courier-quote-changes.submit', $change))->assertRedirect();
|
|
|
|
return $change->fresh();
|
|
}
|
|
|
|
public function test_either_named_regular_reviewer_can_approve_named_initiators_without_admin_grants(): void
|
|
{
|
|
foreach (['review-one' => 'init-one', 'review-two' => 'init-two'] as $reviewer => $initiator) {
|
|
$change = $this->pending($initiator);
|
|
$this->loginAs($this->people[$reviewer], $this->company)
|
|
->get(route('business.courier-quote-changes.show', $change))->assertOk()->assertSeeText('指定审核')->assertSeeText('审核通过');
|
|
$this->post(route('business.courier-quote-changes.approve', $change))->assertRedirect();
|
|
$this->assertSame('approved', $change->fresh()->status);
|
|
$this->assertSame($this->people[$reviewer]->id, $change->fresh()->version->approved_by);
|
|
$this->post(route('business.courier-quote-signatures.prepare', $change->fresh()->version), ['signature_date' => now()->toDateString()])->assertRedirect();
|
|
$this->get(route('business.courier-quote-changes.show', $change))->assertOk()->assertSeeText('推送到 E 签宝')->assertDontSeeText('作废此版本');
|
|
$this->withExceptionHandling()->post(route('business.courier-quote-changes.void', $change), ['void_reason' => '不应获得作废权限'])->assertForbidden();
|
|
$this->withExceptionHandling()->post(route('business.courier-quote-changes.approve', $change))->assertSessionHasErrors('status');
|
|
$this->assertSame('user', $this->people[$reviewer]->roles()->first()->name);
|
|
}
|
|
$this->assertDatabaseCount('business_courier_quote_versions', 2);
|
|
}
|
|
|
|
public function test_admin_and_initiator_cannot_bypass_named_reviewers_or_batch_approval(): void
|
|
{
|
|
$change = $this->pending();
|
|
$this->withExceptionHandling()->post(route('business.courier-quote-changes.approve', $change))->assertForbidden();
|
|
$this->loginAs()->post(route('business.courier-quote-changes.approve', $change))->assertForbidden();
|
|
$this->get(route('business.courier-quote-changes.index'))->assertOk()->assertDontSee('name="ids[]"', false);
|
|
$this->post(route('business.courier-quote-changes.batch-approve'), ['ids' => [$change->id], 'stock_rules_confirmed' => 1])->assertRedirect();
|
|
$this->assertSame('pending_review', $change->fresh()->status);
|
|
$this->assertDatabaseCount('business_courier_quote_versions', 0);
|
|
}
|
|
|
|
public function test_unlisted_admin_cannot_create_change_and_other_initiator_cannot_edit_owners_draft(): void
|
|
{
|
|
$this->withExceptionHandling()->loginAs()->post(route('business.courier-quote-changes.store'), $this->payload())->assertForbidden();
|
|
$this->loginAs($this->people['init-one'], $this->company)->post(route('business.courier-quote-changes.store'), $this->payload())->assertRedirect();
|
|
$change = CourierQuoteChange::firstOrFail();
|
|
$this->loginAs($this->people['init-two'], $this->company)->patch(route('business.courier-quote-changes.update', $change), $this->payload())->assertForbidden();
|
|
$this->assertSame('draft', $change->fresh()->status);
|
|
}
|
|
|
|
public function test_reviewer_can_return_and_revocation_of_identity_or_configuration_is_immediate(): void
|
|
{
|
|
$change = $this->pending();
|
|
$this->loginAs($this->people['review-one'], $this->company)
|
|
->post(route('business.courier-quote-changes.return', $change), ['review_note' => '请核对面单价格'])->assertRedirect();
|
|
$this->assertSame('returned', $change->fresh()->status);
|
|
$this->loginAs($this->people['init-one'], $this->company)->post(route('business.courier-quote-changes.submit', $change))->assertRedirect();
|
|
app(CourierQuoteWorkflowPermissions::class)->save(['initiators' => ['init-one', 'init-two'], 'reviewers' => ['review-two']], 'test admin', $this->user->id);
|
|
$this->withExceptionHandling()->loginAs($this->people['review-one'], $this->company)->post(route('business.courier-quote-changes.approve', $change))->assertForbidden();
|
|
$this->people['review-two']->update(['enabled' => false]);
|
|
$this->assertFalse(app(CourierQuoteWorkflowPermissions::class)->can('reviewers', $this->people['review-two']->id));
|
|
$this->loginAs($this->people['review-two'], $this->company)->post(route('business.courier-quote-changes.approve', $change))->assertRedirect();
|
|
$this->assertDatabaseCount('business_courier_quote_versions', 0);
|
|
}
|
|
|
|
public function test_self_review_stays_blocked_even_when_both_roles_are_assigned(): void
|
|
{
|
|
app(CourierQuoteWorkflowPermissions::class)->save(['initiators' => ['init-one'], 'reviewers' => ['init-one', 'review-one']], 'test admin', $this->user->id);
|
|
$change = $this->pending();
|
|
config(['courier_onboarding.allow_same_reviewer' => true]);
|
|
$this->withExceptionHandling()->post(route('business.courier-quote-changes.approve', $change))->assertSessionHasErrors('approval');
|
|
$this->assertDatabaseCount('business_courier_quote_versions', 0);
|
|
}
|
|
|
|
public function test_onboarded_courier_requote_entry_and_submission_use_the_same_initiator_list(): void
|
|
{
|
|
$quote = ['quote_mode' => 'waybill_rate', 'volumetric_rule' => '计泡', 'volumetric_divisor' => 6000,
|
|
'quote_effective_date' => now()->addDay()->toDateString(),
|
|
'quote_rows' => [['region' => '江苏', 'base' => '3.2', 'rates' => ['0.8']]]];
|
|
$onboarding = CourierOnboarding::create([
|
|
'company_id' => $this->company->id, 'number' => 'RZ-PERMISSIONS', 'token' => str_repeat('q', 40),
|
|
'status' => CourierOnboarding::STATUS_ACTIVATED, 'courier_name' => '权限测试快递',
|
|
...app(CourierOnboardingService::class)->quoteAttributes($quote),
|
|
]);
|
|
$quote['quote_rows'][0]['base'] = '3.5';
|
|
$quote['summary'] = '测试变价发起人员权限';
|
|
foreach (['review-one', 'review-two'] as $id) {
|
|
$this->withExceptionHandling()->loginAs($this->people[$id], $this->company)
|
|
->get(route('business.courier-onboardings.requote', $onboarding))->assertForbidden();
|
|
$this->post(route('business.courier-onboardings.requote.store', $onboarding), $quote)->assertForbidden();
|
|
}
|
|
$this->assertDatabaseCount('business_courier_quote_changes', 0);
|
|
$this->loginAs($this->people['init-two'], $this->company)
|
|
->get(route('business.courier-onboardings.requote', $onboarding))->assertOk();
|
|
$this->post(route('business.courier-onboardings.requote.store', $onboarding), $quote)->assertSessionHasNoErrors()->assertRedirect();
|
|
$change = CourierQuoteChange::firstOrFail();
|
|
$this->assertSame('pending_review', $change->status);
|
|
$this->assertSame($this->people['init-two']->id, $change->created_by);
|
|
}
|
|
|
|
public function test_settings_can_be_saved_and_searched_only_by_administrators_and_are_audited(): void
|
|
{
|
|
$this->loginAs()->get(route('system-settings.workflow-reviewers.index'))->assertOk()->assertSeeText('报价变价发起和审核人员');
|
|
$this->patch(route('system-settings.workflow-reviewers.quote.update'), $this->permissionGroups())->assertRedirect();
|
|
$this->assertCount(2, app(CourierQuoteWorkflowPermissions::class)->history());
|
|
$this->getJson(route('system-settings.workflow-reviewers.search', ['q' => '审核', 'workflow' => 'quote']))->assertOk()->assertJsonPath('people.0.selectable', true);
|
|
$this->withExceptionHandling()->loginAs($this->people['review-one'], $this->company)
|
|
->patch(route('system-settings.workflow-reviewers.quote.update'), $this->permissionGroups())->assertForbidden();
|
|
$this->getJson(route('system-settings.workflow-reviewers.search', ['q' => '审核', 'workflow' => 'quote']))->assertForbidden();
|
|
}
|
|
|
|
public function test_disabled_departed_unbound_and_cross_company_candidates_fail_without_writing(): void
|
|
{
|
|
$this->eligible = false;
|
|
$this->withExceptionHandling()->loginAs()->patch(route('system-settings.workflow-reviewers.quote.update'), $this->permissionGroups())->assertSessionHasErrors('initiators');
|
|
$this->eligible = true;
|
|
$this->patch(route('system-settings.workflow-reviewers.quote.update'), ['initiators' => ['init-one'], 'reviewers' => ['unbound']])->assertSessionHasErrors('reviewers');
|
|
$this->patch(route('system-settings.workflow-reviewers.quote.update'), ['initiators' => ['init-one'], 'reviewers' => ['init-one']])->assertSessionHasErrors('reviewers');
|
|
$other = Company::create(['enabled' => true]);
|
|
$other->makeCurrent();
|
|
$permissions = app(CourierQuoteWorkflowPermissions::class);
|
|
$this->assertFalse($permissions->settings()['configured']);
|
|
try {
|
|
$permissions->save($this->permissionGroups(), 'other admin', null);
|
|
$this->fail('Cross-company people must not be assigned');
|
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
|
$this->assertArrayHasKey('initiators', $e->errors());
|
|
}
|
|
$this->assertCount(0, $permissions->history());
|
|
$this->company->makeCurrent();
|
|
$this->assertCount(1, $permissions->history());
|
|
}
|
|
}
|