Implementa regras de investimento por empresa com dupla aprovacao
This commit is contained in:
@@ -2,11 +2,13 @@
|
||||
|
||||
use App\Enums\PurchasingRole;
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\Budget;
|
||||
use App\Models\CostCenter;
|
||||
use App\Models\CriticalityLevel;
|
||||
use App\Models\Department;
|
||||
use App\Models\Equipment;
|
||||
use App\Models\EquipmentSparePart;
|
||||
use App\Models\InvestmentPolicy;
|
||||
use App\Models\PurchaseAward;
|
||||
use App\Models\PurchaseOrder;
|
||||
use App\Models\Quotation;
|
||||
@@ -18,6 +20,9 @@ use App\Models\Supplier;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Models\Warehouse;
|
||||
use App\Services\BudgetWorkflow;
|
||||
use App\Services\ConflictGuard;
|
||||
use App\Services\EquipmentWorkflow;
|
||||
use App\Services\RequisitionWorkflow;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -60,6 +65,27 @@ function issuePurchaseOrder(object $test, PurchaseAward $award): PurchaseOrder
|
||||
return PurchaseOrder::query()->firstOrFail();
|
||||
}
|
||||
|
||||
function prepareEquipmentInvestment(object $test, string $ceiling = '100.00', bool $includesTax = true): InvestmentPolicy
|
||||
{
|
||||
require_once __DIR__.'/../Support/InvestmentFixture.php';
|
||||
$test->item->update(['purchase_type' => 'equipment', 'production_area' => 'Linha fictícia de investimento']);
|
||||
$test->r->update(['status' => 'draft']);
|
||||
$test->r->refresh();
|
||||
$test->actingAs($test->head);
|
||||
$technical = app(EquipmentWorkflow::class);
|
||||
$technical->review($test->head, $test->r, $technical->basis($test->r), 'approved', 'Validação técnica fictícia independente.');
|
||||
$test->r->update(['status' => 'approved']);
|
||||
$test->r->steps()->create(['tenant_id' => $test->tenant->id, 'position' => 2, 'role' => 'finance', 'decision' => 'approved', 'approver_id' => $test->finance->id, 'decided_at' => now()]);
|
||||
$test->actingAs($test->owner);
|
||||
Budget::query()->create(['tenant_id' => $test->tenant->id, 'cost_center_id' => $test->r->cost_center_id, 'year' => now()->year, 'kind' => 'investment', 'amount_minor' => 100000, 'includes_tax' => true, 'created_by' => $test->admin->id, 'approved_by' => $test->finance->id, 'reason' => 'Dotação fictícia de investimento']);
|
||||
DB::transaction(function () use ($test) {
|
||||
app(ConflictGuard::class)->lockCompany($test->owner);
|
||||
app(BudgetWorkflow::class)->record($test->owner, $test->r, 'reserve', $test->r->total_minor, $test->r->subtotal_minor);
|
||||
});
|
||||
|
||||
return publishInvestmentFixture($test->tenant->id, $ceiling, $includesTax);
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
$this->tenant = Tenant::query()->create(['name' => 'Empresa das ordens']);
|
||||
$this->department = Department::query()->create(['tenant_id' => $this->tenant->id, 'name' => 'Produção']);
|
||||
@@ -402,3 +428,61 @@ test('spare purchase flows through award order receipt and issue with equipment
|
||||
$this->postJson('/stock/operacoes', ['kind' => 'issue', 'reference' => 'SAI-FLOW', 'idempotency_key' => (string) Str::uuid(), 'warehouse_id' => $warehouse->id, 'stock_item_id' => $stock->id, 'responsible_id' => $receiver->id, 'destination' => 'Produção', 'reason' => 'Montagem no equipamento', 'quantity' => '0.500', 'equipment_id' => $asset->id])->assertCreated();
|
||||
expect((int) StockMovement::query()->where('stock_item_id', $stock->id)->sum('quantity_milli'))->toBe(500)->and(StockMovement::query()->where('equipment_id', $asset->id)->count())->toBe(2);
|
||||
});
|
||||
|
||||
test('equipment within the explicit inclusive investment ceiling requires finance and flows through receipt', function () {
|
||||
$policy = prepareEquipmentInvestment($this);
|
||||
$award = selectPurchaseAward($this);
|
||||
expect($award->approvals()->pluck('role')->all())->toBe(['department_head', 'finance']);
|
||||
$order = issuePurchaseOrder($this, $award);
|
||||
expect($order->snapshot['investment_rules']['id'])->toBe($policy->id)
|
||||
->and($order->snapshot['investment_rules']['ceiling_minor'])->toBe(10000)
|
||||
->and($order->snapshot['equipment_requests'][0]['production_area'])->toBe('Linha fictícia de investimento');
|
||||
$receiver = User::factory()->create(['tenant_id' => $this->tenant->id, 'purchasing_role' => PurchasingRole::Warehouse]);
|
||||
$this->actingAs($receiver);
|
||||
$warehouse = Warehouse::query()->create(['tenant_id' => $this->tenant->id, 'created_by' => $receiver->id, 'code' => 'INV', 'name' => 'Armazém fictício']);
|
||||
$stock = StockItem::query()->create(['tenant_id' => $this->tenant->id, 'created_by' => $receiver->id, 'code' => 'EQ-INV', 'name' => 'Equipamento recebido fictício', 'unit' => 'un']);
|
||||
$this->postJson('/ordens/'.$order->id.'/recepcoes', ['idempotency_key' => (string) Str::uuid(), 'delivery_note' => 'GR-INV', 'received_on' => today()->format('Y-m-d'), 'warehouse_id' => $warehouse->id, 'items' => [['purchase_order_item_id' => $order->items()->firstOrFail()->id, 'received_quantity' => '1.000', 'accepted_quantity' => '1.000', 'quality_status' => 'compliant', 'stock_item_id' => $stock->id]]])->assertCreated();
|
||||
expect((int) StockMovement::query()->where('stock_item_id', $stock->id)->sum('quantity_milli'))->toBe(1000);
|
||||
publishInvestmentFixture($this->tenant->id, '200.00');
|
||||
$this->actingAs($this->buyer)->post('/adjudicacoes/'.$award->id.'/emitir')->assertSessionHasNoErrors();
|
||||
expect(PurchaseOrder::query()->count())->toBe(1)->and($order->refresh()->snapshot['investment_rules']['ceiling_minor'])->toBe(10000);
|
||||
});
|
||||
|
||||
test('equipment above the company investment ceiling cannot be awarded', function () {
|
||||
prepareEquipmentInvestment($this, '99.99');
|
||||
$this->actingAs($this->buyer)->post($this->awardUrl, $this->selection)->assertSessionHasErrors('investment');
|
||||
expect(PurchaseAward::query()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('investment tax basis is explicit and independent of ordinary approval tiers', function (bool $includesTax) {
|
||||
awardPrices($this, 10000, 16);
|
||||
prepareEquipmentInvestment($this, '100.00', $includesTax);
|
||||
if ($includesTax) {
|
||||
$this->actingAs($this->buyer)->post($this->awardUrl, $this->selection)->assertSessionHasErrors('investment');
|
||||
} else {
|
||||
$order = issuePurchaseOrder($this, selectPurchaseAward($this));
|
||||
expect($order->total_minor)->toBe(11600)->and($order->snapshot['investment_rules']['includes_tax'])->toBeFalse();
|
||||
}
|
||||
})->with([true, false]);
|
||||
|
||||
test('a newly published investment policy cannot reuse an existing final approval cycle', function () {
|
||||
prepareEquipmentInvestment($this);
|
||||
$award = selectPurchaseAward($this);
|
||||
approvePurchaseAward($this, $award);
|
||||
publishInvestmentFixture($this->tenant->id, '200.00');
|
||||
$this->actingAs($this->buyer)->post('/adjudicacoes/'.$award->id.'/emitir')->assertSessionHasErrors('investment');
|
||||
expect(PurchaseOrder::query()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('equipment cannot bypass technical finance or investment budget requirements', function (string $missing) {
|
||||
prepareEquipmentInvestment($this);
|
||||
if ($missing === 'technical') {
|
||||
DB::table('technical_reviews')->where('requisition_id', $this->r->id)->delete();
|
||||
} elseif ($missing === 'finance') {
|
||||
$this->r->steps()->where('role', 'finance')->delete();
|
||||
} else {
|
||||
DB::table('budgets')->where('tenant_id', $this->tenant->id)->update(['kind' => 'operating']);
|
||||
}
|
||||
$this->actingAs($this->buyer)->post($this->awardUrl, $this->selection)->assertSessionHasErrors($missing === 'technical' ? 'technical' : ($missing === 'finance' ? 'approvals' : 'investment'));
|
||||
expect(PurchaseAward::query()->count())->toBe(0);
|
||||
})->with(['technical', 'finance', 'budget']);
|
||||
|
||||
Reference in New Issue
Block a user