报价历史:支持勾选并批量审核通过 (#387)
This commit is contained in:
parent
2d30aea624
commit
e428cf5de2
@ -165,6 +165,41 @@ class CourierQuoteChanges extends Controller
|
|||||||
->with('success', $message);
|
->with('success', $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function batchApprove(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
abort_unless($this->canReview(), 403, '当前账号没有报价审核权限。');
|
||||||
|
$input = $request->validate([
|
||||||
|
'ids' => ['required', 'array', 'min:1', 'max:100'],
|
||||||
|
'ids.*' => ['required', 'integer', 'min:1', 'distinct'],
|
||||||
|
'stock_rules_confirmed' => ['accepted'],
|
||||||
|
], ['stock_rules_confirmed.accepted' => '请先核对选中报价的原报价、计费规则及实际执行日期。']);
|
||||||
|
$results = [];
|
||||||
|
// 历史签署资料需要逐份核对,不接受批量标记已经核验。
|
||||||
|
$singleRequest = $request->duplicate();
|
||||||
|
$singleRequest->merge(['stock_signature_verified' => false]);
|
||||||
|
foreach ($input['ids'] as $id) {
|
||||||
|
$change = CourierQuoteChange::query()->where('company_id', company_id())->find($id);
|
||||||
|
if ($change === null) {
|
||||||
|
$results[] = ['number' => '所选记录', 'success' => false, 'message' => '记录已不存在或不属于当前公司。'];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 复用单条入口,权限、校验、版本、日志及后续签署流程保持一致。
|
||||||
|
$response = $this->approve($singleRequest, $change);
|
||||||
|
$results[] = ['number' => $change->number, 'success' => true, 'message' => $response->getSession()->get('success')];
|
||||||
|
} catch (\Illuminate\Validation\ValidationException $exception) {
|
||||||
|
$results[] = ['number' => $change->number, 'success' => false, 'message' => collect($exception->errors())->flatten()->implode(';')];
|
||||||
|
} catch (\Throwable $exception) {
|
||||||
|
report($exception);
|
||||||
|
$results[] = ['number' => $change->number, 'success' => false, 'message' => '处理失败,请查看该单状态并重试。'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$passed = count(array_filter($results, static fn ($result) => $result['success']));
|
||||||
|
return redirect()->route('business.courier-quote-changes.index', ['page' => max(1, (int) $request->input('page', 1))])
|
||||||
|
->with('success', '批量审核完成:成功 ' . $passed . ' 条,未通过 ' . (count($results) - $passed) . ' 条。')
|
||||||
|
->with('batch_review_results', $results);
|
||||||
|
}
|
||||||
|
|
||||||
/** 已入驻快递的变价版本按入驻资料(重新)推送到快递 e签宝。 */
|
/** 已入驻快递的变价版本按入驻资料(重新)推送到快递 e签宝。 */
|
||||||
public function pushToCourier(CourierQuoteChange $quoteChange): RedirectResponse
|
public function pushToCourier(CourierQuoteChange $quoteChange): RedirectResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@ -141,6 +141,10 @@ final class CourierQuoteChangeService
|
|||||||
$source = $this->findSourceQuote($change->source_quote_id);
|
$source = $this->findSourceQuote($change->source_quote_id);
|
||||||
|
|
||||||
return DB::transaction(function () use ($change, $source, $userId, $historicalDocumentsConfirmed): CourierQuoteChange {
|
return DB::transaction(function () use ($change, $source, $userId, $historicalDocumentsConfirmed): CourierQuoteChange {
|
||||||
|
$change = CourierQuoteChange::query()->lockForUpdate()->findOrFail($change->id);
|
||||||
|
if ($change->status !== CourierQuoteChange::STATUS_PENDING_REVIEW) {
|
||||||
|
throw ValidationException::withMessages(['status' => '只有待审核的变更单可以审核通过,请刷新状态。']);
|
||||||
|
}
|
||||||
if ($change->isStockQuote()) {
|
if ($change->isStockQuote()) {
|
||||||
\App\Models\Common\Company::whereKey(company_id())->lockForUpdate()->firstOrFail();
|
\App\Models\Common\Company::whereKey(company_id())->lockForUpdate()->firstOrFail();
|
||||||
$entry = CourierStockQuoteEntry::findOrFail(data_get($change->quote_payload, 'stock_quote_entry_id'));
|
$entry = CourierStockQuoteEntry::findOrFail(data_get($change->quote_payload, 'stock_quote_entry_id'));
|
||||||
|
|||||||
11
docs/报价历史批量审核.md
Normal file
11
docs/报价历史批量审核.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# 报价历史批量审核
|
||||||
|
|
||||||
|
报价管理 → 报价历史中,有报价审核权限的账号可勾选本页待审核记录,或用表头勾选框全选本页可审核项,点击“批量审核通过”。确认窗口列出变更单号、快递、变更类型和执行期间;核对原报价、计费规则及实际执行日期后提交。已审核和受自审限制的记录不显示勾选框。
|
||||||
|
|
||||||
|
批量接口沿用当前公司和报价管理页级权限,同时检查管理员/超级管理员审核身份;仅处理明确提交的记录ID,最多100条,不隐式选择其他页或其他公司。逐条调用现有单条审核入口,保留自审限制、报价期间冲突校验、版本生成、审核日志和原签署后续流程。单条失败不撤销其他成功项;完成后显示成功/未通过数量及每条结果,返回原页码。
|
||||||
|
|
||||||
|
存量报价仍按既有规则审核生效,不自动发起签署;历史签署资料必须逐份核对,不接受批量标记为已经核验。非存量变价沿用原单条审核的签署准备与推送流程。
|
||||||
|
|
||||||
|
审核服务在事务内锁定记录并重新检查待审核状态,防止过期模型或重复提交再次生成版本。无数据库迁移,无预先审核正式数据。
|
||||||
|
|
||||||
|
验证:QuoteBatchReviewTest覆盖多条存量报价、未选项保持原状、历史核验标记隔离、部分失败、已审核/自审/跨公司/不存在记录、确认与输入边界、无审核权限、过期模型重复审核;同步回归单条正常审核和自审限制。
|
||||||
@ -38,19 +38,61 @@
|
|||||||
<div class="quote-history__actions"><span class="quote-history__hint">变价请在报价管理的报价列表里,点对应报价那一行的「变价」。</span></div>
|
<div class="quote-history__actions"><span class="quote-history__hint">变价请在报价管理的报价列表里,点对应报价那一行的「变价」。</span></div>
|
||||||
</div>
|
</div>
|
||||||
@if(is_string(session('success')))<div style="margin-bottom:16px;border:1px solid #BAD0AB;border-radius:8px;background:#F3F8EF;padding:10px 12px;color:#40622F" role="status">{{ session('success') }}</div>@endif
|
@if(is_string(session('success')))<div style="margin-bottom:16px;border:1px solid #BAD0AB;border-radius:8px;background:#F3F8EF;padding:10px 12px;color:#40622F" role="status">{{ session('success') }}</div>@endif
|
||||||
|
@if($errors->any())<div class="qh-review-error" role="alert">@foreach($errors->all() as $error)<div>{{ $error }}</div>@endforeach</div>@endif
|
||||||
|
@if(session('batch_review_results'))
|
||||||
|
<details class="qh-review-results" open><summary>批量审核逐条结果</summary><ul>@foreach(session('batch_review_results') as $result)<li><strong>{{ $result['number'] }}</strong> · {{ $result['success'] ? '审核成功' : '未通过' }} · {{ $result['message'] }}</li>@endforeach</ul></details>
|
||||||
|
@endif
|
||||||
<section class="quote-history__panel" aria-labelledby="quote-history-title">
|
<section class="quote-history__panel" aria-labelledby="quote-history-title">
|
||||||
<header class="quote-history__header"><div class="quote-history__title" id="quote-history-title"><span class="material-icons-outlined" aria-hidden="true">history</span><span>报价变更记录</span></div><div class="quote-history__hint">草稿、审核和新版本按当前公司隔离保存,历史记录不可覆盖原报价。</div></header>
|
<header class="quote-history__header"><div class="quote-history__title" id="quote-history-title"><span class="material-icons-outlined" aria-hidden="true">history</span><span>报价变更记录</span></div><div class="quote-history__hint">草稿、审核和新版本按当前公司隔离保存,历史记录不可覆盖原报价。</div></header>
|
||||||
@if($changes->isEmpty())
|
@if($changes->isEmpty())
|
||||||
<div class="quote-history__empty"><span class="material-icons-outlined" aria-hidden="true">difference</span><strong>还没有报价变更记录</strong><div>发起第一张变更单后,草稿和审核进度会显示在这里。</div></div>
|
<div class="quote-history__empty"><span class="material-icons-outlined" aria-hidden="true">difference</span><strong>还没有报价变更记录</strong><div>发起第一张变更单后,草稿和审核进度会显示在这里。</div></div>
|
||||||
@else
|
@else
|
||||||
<div class="quote-history__table-wrap"><table><thead><tr><th>变更单号</th><th>原报价</th><th>快递公司</th><th>变更类型</th><th>明细</th><th>生效日期</th><th>生效方式</th><th>追溯核算</th><th>状态</th><th>新版本</th><th>更新时间</th><th>操作</th></tr></thead><tbody>
|
@if($canReview)
|
||||||
|
<form method="post" action="{{ route('business.courier-quote-changes.batch-approve') }}" data-batch-review>
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="page" value="{{ $changes->currentPage() }}">
|
||||||
|
<div class="qh-review-toolbar"><span>已选 <strong data-review-count>0</strong> 条</span><button type="button" class="quote-history__button quote-history__button--primary" data-review-open disabled>批量审核通过</button><span class="quote-history__hint">可全选本页待审核项;逐条校验,失败项保留待处理。</span></div>
|
||||||
|
@endif
|
||||||
|
<div class="quote-history__table-wrap"><table><thead><tr>@if($canReview)<th><input type="checkbox" data-review-all aria-label="全选本页可审核记录"></th>@endif<th>变更单号</th><th>原报价</th><th>快递公司</th><th>变更类型</th><th>明细</th><th>生效日期</th><th>生效方式</th><th>追溯核算</th><th>状态</th><th>新版本</th><th>更新时间</th><th>操作</th></tr></thead><tbody>
|
||||||
@foreach($changes as $change)
|
@foreach($changes as $change)
|
||||||
@php($badge = match($change->status){'pending_review'=>'pending','approved'=>'approved','returned'=>'returned',default=>''})
|
@php($badge = match($change->status){'pending_review'=>'pending','approved'=>'approved','returned'=>'returned',default=>''})
|
||||||
<tr><td class="quote-history__number">{{ $change->number }}</td><td>{{ $change->source_quote_id }}</td><td>{{ $change->carrier_name }}</td><td>{{ $change->change_type }}</td><td>{{ $change->items->count() }} 项</td><td>{{ $change->effective_date->toDateString() }}</td><td><span class="quote-history__badge @if($change->isRetrospective()) quote-history__badge--pending @endif">{{ $change->isRetrospective() ? '追溯变价' : '正常生效' }}</span></td><td><span class="quote-history__badge @if($change->recalculation_status==='blocked') quote-history__badge--returned @elseif($change->recalculation_status==='pending') quote-history__badge--pending @endif">{{ $change->recalculationStatusLabel() }}</span></td><td><span class="quote-history__badge @if($badge) quote-history__badge--{{ $badge }} @endif">{{ $change->statusLabel() }}</span></td><td>{{ $change->version?->version_code ?? '—' }}</td><td>{{ $change->updated_at?->format('Y-m-d H:i') }}</td><td><a class="quote-history__link" href="{{ route('business.courier-quote-changes.show',$change) }}">查看</a>@if($change->isEditable() && $change->created_by===user_id()) · <a class="quote-history__link" href="{{ route('business.courier-quote-changes.edit',$change) }}">编辑</a>@endif</td></tr>
|
<tr>@if($canReview)<td>@if($change->status === 'pending_review' && ($change->created_by !== user_id() || config('courier_onboarding.allow_same_reviewer')))<input type="checkbox" name="ids[]" value="{{ $change->id }}" data-review-choice data-review-label="{{ $change->number }} · {{ $change->carrier_name }} · {{ $change->change_type }} · {{ $change->periodLabel() }}" aria-label="选择 {{ $change->number }}">@else<span class="quote-history__hint" title="{{ $change->status === 'pending_review' ? '发起人不能审核自己提交的报价' : '仅待审核记录可勾选' }}">—</span>@endif</td>@endif<td class="quote-history__number">{{ $change->number }}</td><td>{{ $change->source_quote_id }}</td><td>{{ $change->carrier_name }}</td><td>{{ $change->change_type }}</td><td>{{ $change->items->count() }} 项</td><td>{{ $change->effective_date->toDateString() }}</td><td><span class="quote-history__badge @if($change->isRetrospective()) quote-history__badge--pending @endif">{{ $change->isRetrospective() ? '追溯变价' : '正常生效' }}</span></td><td><span class="quote-history__badge @if($change->recalculation_status==='blocked') quote-history__badge--returned @elseif($change->recalculation_status==='pending') quote-history__badge--pending @endif">{{ $change->recalculationStatusLabel() }}</span></td><td><span class="quote-history__badge @if($badge) quote-history__badge--{{ $badge }} @endif">{{ $change->statusLabel() }}</span></td><td>{{ $change->version?->version_code ?? '—' }}</td><td>{{ $change->updated_at?->format('Y-m-d H:i') }}</td><td><a class="quote-history__link" href="{{ route('business.courier-quote-changes.show',$change) }}">查看</a>@if($change->isEditable() && $change->created_by===user_id()) · <a class="quote-history__link" href="{{ route('business.courier-quote-changes.edit',$change) }}">编辑</a>@endif</td></tr>
|
||||||
@endforeach
|
@endforeach
|
||||||
</tbody></table></div><div class="quote-history__pagination">{{ $changes->links() }}</div>
|
</tbody></table></div>
|
||||||
|
@if($canReview)
|
||||||
|
<dialog class="qh-review-dialog" data-review-dialog aria-labelledby="qh-review-dialog-title">
|
||||||
|
<h3 id="qh-review-dialog-title">确认批量审核通过</h3>
|
||||||
|
<p>将审核下列报价,并按现有流程生成新版本。请先核对原报价、计费规则与实际执行日期。</p>
|
||||||
|
<ul data-review-selection></ul>
|
||||||
|
<p>存量报价审核后生效,不自动发起签署;其他变价沿用单条审核的签署准备和推送流程。历史签署资料不会在此批量标记为已核验。</p>
|
||||||
|
<label class="qh-review-confirm"><input type="checkbox" name="stock_rules_confirmed" value="1" required>我已核对所有选中报价的原报价、计费规则及实际执行日期</label>
|
||||||
|
<div class="qh-review-dialog-actions"><button type="button" class="quote-history__button" data-review-cancel>取消</button><button type="submit" class="quote-history__button quote-history__button--primary" data-review-submit>确认审核通过</button></div>
|
||||||
|
</dialog>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
<div class="quote-history__pagination">{{ $changes->links() }}</div>
|
||||||
@endif
|
@endif
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
<style>
|
||||||
|
.qh-review-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:12px;padding:12px 16px;border-bottom:1px solid #D8D2C7}.qh-review-toolbar strong{color:#856632}.quote-history input[type=checkbox]{width:16px;height:16px;accent-color:#856632;vertical-align:middle}.quote-history__button:disabled{opacity:.55;cursor:not-allowed}.qh-review-dialog{width:min(680px,calc(100vw - 32px));padding:24px;border:1px solid #D8D2C7;border-radius:12px;color:#171717;background:#fff;font:inherit}.qh-review-dialog::backdrop{background:#17171766}.qh-review-dialog h3{margin:0 0 12px;font-size:14px;font-weight:600}.qh-review-dialog p{font-size:12px;line-height:1.7;color:#595959}.qh-review-dialog ul{max-height:35vh;overflow:auto;padding-left:20px;line-height:1.8}.qh-review-confirm{display:flex;gap:8px;align-items:flex-start;font-size:12px;line-height:1.7}.qh-review-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.qh-review-results{padding:12px 16px;margin-bottom:16px;border:1px solid #D8D2C7;border-radius:8px;background:#fff;font-size:12px;line-height:1.7}.qh-review-results ul{max-height:260px;overflow:auto}.qh-review-error{padding:12px;margin-bottom:16px;border:1px solid #E6B7B7;border-radius:8px;background:#FFF4F4;color:#B80000}.quote-history__table-wrap{max-height:60vh;overflow:auto}.quote-history th{position:sticky;top:0;z-index:1}.quote-history :focus-visible{outline:2px solid #856632;outline-offset:2px}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const form = document.querySelector('[data-batch-review]');
|
||||||
|
if (!form) return;
|
||||||
|
const all = form.querySelector('[data-review-all]');
|
||||||
|
const choices = Array.from(form.querySelectorAll('[data-review-choice]'));
|
||||||
|
const open = form.querySelector('[data-review-open]');
|
||||||
|
const dialog = form.querySelector('[data-review-dialog]');
|
||||||
|
const update = () => { const checked = choices.filter(input => input.checked); form.querySelector('[data-review-count]').textContent = checked.length; open.disabled = checked.length === 0; all.disabled = choices.length === 0; all.checked = choices.length > 0 && checked.length === choices.length; all.indeterminate = checked.length > 0 && checked.length < choices.length; };
|
||||||
|
all.addEventListener('change', () => { choices.forEach(input => input.checked = all.checked); update(); });
|
||||||
|
choices.forEach(input => input.addEventListener('change', update));
|
||||||
|
open.addEventListener('click', () => { const list = form.querySelector('[data-review-selection]'); list.replaceChildren(); choices.filter(input => input.checked).forEach(input => { const item = document.createElement('li'); item.textContent = input.dataset.reviewLabel; list.append(item); }); form.elements.stock_rules_confirmed.checked = false; dialog.showModal(); });
|
||||||
|
form.querySelector('[data-review-cancel]').addEventListener('click', () => dialog.close());
|
||||||
|
form.addEventListener('submit', event => { if (!choices.some(input => input.checked)) { event.preventDefault(); return; } form.querySelector('[data-review-submit]').disabled = true; form.querySelector('[data-review-submit]').textContent = '审核中…'; open.disabled = true; });
|
||||||
|
update();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
</x-layouts.admin>
|
</x-layouts.admin>
|
||||||
|
|||||||
@ -152,6 +152,7 @@ Route::prefix('workspace/courier-payables/quote-management/changes')
|
|||||||
// 不以 create 结尾:Akaunting 的套餐限制中间件会把 …/create 的 GET 请求跳到用户页。
|
// 不以 create 结尾:Akaunting 的套餐限制中间件会把 …/create 的 GET 请求跳到用户页。
|
||||||
Route::get('new', [CourierQuoteChanges::class, 'create'])->name('start');
|
Route::get('new', [CourierQuoteChanges::class, 'create'])->name('start');
|
||||||
Route::post('/', [CourierQuoteChanges::class, 'store'])->name('store');
|
Route::post('/', [CourierQuoteChanges::class, 'store'])->name('store');
|
||||||
|
Route::post('batch-approve', [CourierQuoteChanges::class, 'batchApprove'])->name('batch-approve');
|
||||||
Route::get('{quoteChange}', [CourierQuoteChanges::class, 'show'])->name('show');
|
Route::get('{quoteChange}', [CourierQuoteChanges::class, 'show'])->name('show');
|
||||||
Route::get('{quoteChange}/edit', [CourierQuoteChanges::class, 'edit'])->name('edit');
|
Route::get('{quoteChange}/edit', [CourierQuoteChanges::class, 'edit'])->name('edit');
|
||||||
Route::patch('{quoteChange}', [CourierQuoteChanges::class, 'update'])->name('update');
|
Route::patch('{quoteChange}', [CourierQuoteChanges::class, 'update'])->name('update');
|
||||||
|
|||||||
106
tests/Feature/Business/QuoteBatchReviewTest.php
Normal file
106
tests/Feature/Business/QuoteBatchReviewTest.php
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Business;
|
||||||
|
|
||||||
|
use App\Models\Auth\Permission;
|
||||||
|
use App\Models\Auth\Role;
|
||||||
|
use App\Models\Business\CourierQuoteChange;
|
||||||
|
use App\Models\Business\CourierQuoteVersion;
|
||||||
|
use App\Support\CourierQuoteChangeService;
|
||||||
|
use App\Support\CourierStockQuoteEntryService;
|
||||||
|
use App\Support\QuoteManagementSnapshot;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Tests\Feature\FeatureTestCase;
|
||||||
|
|
||||||
|
class QuoteBatchReviewTest extends FeatureTestCase
|
||||||
|
{
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
Http::preventStrayRequests();
|
||||||
|
app()->instance(QuoteManagementSnapshot::class, new QuoteManagementSnapshot('/nonexistent-batch-review-test.json'));
|
||||||
|
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||||
|
config(['courier_onboarding.allow_same_reviewer' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function reviewer()
|
||||||
|
{
|
||||||
|
$reviewer = user_model_class()::create(['name'=>'批量审核员','email'=>'batch-'.uniqid().'@example.com','password'=>'local-test-password','locale'=>'zh-CN','enabled'=>1,'landing_page'=>'dashboard','created_from'=>'core::test']);
|
||||||
|
$reviewer->companies()->attach($this->company->id);
|
||||||
|
$reviewer->attachRole(Role::where('name','admin')->firstOrFail());
|
||||||
|
$reviewer->attachPermission(Permission::where('name','read-admin-panel')->firstOrFail());
|
||||||
|
return $reviewer;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function pending(string $name, ?int $creator = null): CourierQuoteChange
|
||||||
|
{
|
||||||
|
$creator ??= $this->user->id;
|
||||||
|
$entry = app(CourierStockQuoteEntryService::class)->createPrepared(
|
||||||
|
['quote_type'=>'标准快递报价','effective_date'=>'2026-08-01','summary'=>'批量审核测试','billing_rules'=>'保留原报价规则'],
|
||||||
|
$creator, ['code'=>'TEST-'.$name,'name'=>$name],
|
||||||
|
['sheet_name'=>'原报价','source_range'=>'A1:B2','values'=>[['省份','运费'],['江苏',5]],'formulas'=>[[],[]]]
|
||||||
|
);
|
||||||
|
return app(CourierQuoteChangeService::class)->submit($entry->change, $creator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_batch_approves_selected_stock_quotes_and_does_not_mark_historical_files_verified(): void
|
||||||
|
{
|
||||||
|
$this->loginAs();
|
||||||
|
$one=$this->pending('快递甲');$two=$this->pending('快递乙');$untouched=$this->pending('未选快递');
|
||||||
|
$this->loginAs($this->reviewer())->post(route('business.courier-quote-changes.batch-approve'), ['ids'=>[$one->id,$two->id],'stock_rules_confirmed'=>1,'stock_signature_verified'=>1])
|
||||||
|
->assertRedirect(route('business.courier-quote-changes.index',['page'=>1]))
|
||||||
|
->assertSessionHas('success','批量审核完成:成功 2 条,未通过 0 条。');
|
||||||
|
$this->assertSame('approved',$one->fresh()->status);
|
||||||
|
$this->assertSame('approved',$two->fresh()->status);
|
||||||
|
$this->assertSame('pending_review',$untouched->fresh()->status);
|
||||||
|
$this->assertDatabaseCount('business_courier_quote_versions',2);
|
||||||
|
foreach(CourierQuoteVersion::all() as $version) $this->assertSame('存量报价,审核生效(无需签章)',$version->signature_status);
|
||||||
|
$this->assertSame(1,$one->events()->where('event_type','approved')->count());
|
||||||
|
$this->assertSame(1,$two->events()->where('event_type','approved')->count());
|
||||||
|
Http::assertNothingSent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_partial_batch_keeps_self_review_stale_and_other_company_records_unchanged(): void
|
||||||
|
{
|
||||||
|
$this->loginAs();$reviewer=$this->reviewer();
|
||||||
|
$good=$this->pending('正常');$own=$this->pending('自审',$reviewer->id);$old=$this->pending('已审核');$foreign=$this->pending('外公司');
|
||||||
|
app(CourierQuoteChangeService::class)->approve($old,$reviewer->id);
|
||||||
|
DB::table('business_courier_quote_changes')->where('id',$foreign->id)->update(['company_id'=>$this->company->id+1000]);
|
||||||
|
$this->loginAs($reviewer)->post(route('business.courier-quote-changes.batch-approve'), ['ids'=>[$good->id,$own->id,$old->id,$foreign->id,99999999],'stock_rules_confirmed'=>1])
|
||||||
|
->assertSessionHas('success','批量审核完成:成功 1 条,未通过 4 条。');
|
||||||
|
$this->assertSame('approved',$good->fresh()->status);
|
||||||
|
$this->assertSame('pending_review',$own->fresh()->status);
|
||||||
|
$this->assertDatabaseCount('business_courier_quote_versions',2);
|
||||||
|
$this->assertSame('pending_review',DB::table('business_courier_quote_changes')->where('id',$foreign->id)->value('status'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_batch_requires_confirmation_nonempty_distinct_bounded_ids(): void
|
||||||
|
{
|
||||||
|
$this->loginAs();$change=$this->pending('校验');
|
||||||
|
$this->withExceptionHandling()->loginAs($this->reviewer());
|
||||||
|
foreach ([['ids'=>[$change->id]],['ids'=>[],'stock_rules_confirmed'=>1],['ids'=>[$change->id,$change->id],'stock_rules_confirmed'=>1],['ids'=>range(1,101),'stock_rules_confirmed'=>1]] as $input) {
|
||||||
|
$this->post(route('business.courier-quote-changes.batch-approve'),$input)->assertSessionHasErrors();
|
||||||
|
}
|
||||||
|
$this->assertSame('pending_review',$change->fresh()->status);
|
||||||
|
$this->assertDatabaseCount('business_courier_quote_versions',0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_non_reviewer_cannot_use_batch_endpoint(): void
|
||||||
|
{
|
||||||
|
$this->loginAs();$change=$this->pending('权限');$viewer=$this->reviewer();$viewer->detachRole(Role::where('name','admin')->firstOrFail());
|
||||||
|
$this->withExceptionHandling()->loginAs($viewer)->post(route('business.courier-quote-changes.batch-approve'),['ids'=>[$change->id],'stock_rules_confirmed'=>1])->assertForbidden();
|
||||||
|
$this->assertSame('pending_review',$change->fresh()->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_stale_model_cannot_approve_twice(): void
|
||||||
|
{
|
||||||
|
$this->loginAs();$one=$this->pending('并发');$stale=CourierQuoteChange::findOrFail($one->id);$reviewer=$this->reviewer();
|
||||||
|
app(CourierQuoteChangeService::class)->approve($one,$reviewer->id);
|
||||||
|
try { app(CourierQuoteChangeService::class)->approve($stale,$reviewer->id); $this->fail('过期状态不应重复审核'); }
|
||||||
|
catch(ValidationException $exception){$this->assertArrayHasKey('status',$exception->errors());}
|
||||||
|
$this->assertDatabaseCount('business_courier_quote_versions',1);
|
||||||
|
$this->assertSame(1,$one->events()->where('event_type','approved')->count());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user