@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property string $role
|
||||
* @property string $decision
|
||||
* @property int|null $approver_id
|
||||
*/
|
||||
class ApprovalStep extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use LogicException;
|
||||
|
||||
class AuditLog extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['before' => 'array', 'after' => 'array'];
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::updating(fn () => throw new LogicException('O histórico de auditoria não pode ser alterado.'));
|
||||
static::deleting(fn () => throw new LogicException('O histórico de auditoria não pode ser eliminado.'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
trait BelongsToTenant
|
||||
{
|
||||
protected static function bootBelongsToTenant(): void
|
||||
{
|
||||
static::addGlobalScope('tenant', function (Builder $query) {
|
||||
$tenantId = auth()->check() ? auth()->user()->tenant_id : null;
|
||||
$query->where($query->getModel()->qualifyColumn('tenant_id'), $tenantId ?? 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use LogicException;
|
||||
|
||||
trait ImmutablePurchaseRecord
|
||||
{
|
||||
protected static function bootImmutablePurchaseRecord(): void
|
||||
{
|
||||
static::updating(fn () => throw new LogicException('O documento emitido não pode ser reescrito.'));
|
||||
static::deleting(fn () => throw new LogicException('O documento emitido deve ser preservado.'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\PurchasingRole;
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use LogicException;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $user_id
|
||||
* @property int|null $supplier_id
|
||||
* @property int|null $requisition_id
|
||||
* @property int|null $root_id
|
||||
* @property bool $has_conflict
|
||||
*/
|
||||
class ConflictDeclaration extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['has_conflict' => 'boolean', 'known_since' => 'date:Y-m-d', 'declared_at' => 'datetime'];
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::updating(fn () => throw new LogicException('As declarações são preservadas; registe uma correcção.'));
|
||||
static::deleting(fn () => throw new LogicException('As declarações de conflito não podem ser eliminadas.'));
|
||||
}
|
||||
|
||||
/** @param Builder<ConflictDeclaration> $query */
|
||||
public function scopeVisibleTo(Builder $query, User $user): void
|
||||
{
|
||||
if (! in_array($user->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Finance])) {
|
||||
$query->where('user_id', $user->id);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return BelongsTo<User, $this> */
|
||||
public function author(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
/** @return BelongsTo<Supplier, $this> */
|
||||
public function supplier(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Supplier::class);
|
||||
}
|
||||
|
||||
/** @return BelongsTo<Requisition, $this> */
|
||||
public function requisition(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Requisition::class);
|
||||
}
|
||||
|
||||
/** @return HasOne<ConflictDeclaration, $this> */
|
||||
public function successor(): HasOne
|
||||
{
|
||||
return $this->hasOne(self::class, 'previous_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $department_id
|
||||
*/
|
||||
class CostCenter extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/** @property int $id */
|
||||
class Department extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property string $quality
|
||||
* @property string $history
|
||||
* @property string $payment
|
||||
*/
|
||||
class ProposalEvaluation extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['no_conflict' => 'boolean', 'quality' => 'decimal:2', 'history' => 'decimal:2', 'payment' => 'decimal:2'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $requisition_id
|
||||
* @property int $quotation_id
|
||||
* @property int $created_by
|
||||
* @property string $status
|
||||
* @property string $comparison_hash
|
||||
* @property array<string, mixed> $approval_snapshot
|
||||
* @property list<array<string, mixed>> $decision_matrix
|
||||
*/
|
||||
class PurchaseAward extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['approval_snapshot' => 'array', 'decision_matrix' => 'array', 'delivery_date' => 'date:Y-m-d'];
|
||||
}
|
||||
|
||||
/** @return HasMany<PurchaseAwardApproval, $this> */
|
||||
public function approvals(): HasMany
|
||||
{
|
||||
return $this->hasMany(PurchaseAwardApproval::class)->orderBy('position');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PurchaseAwardApproval extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use App\Models\Concerns\ImmutablePurchaseRecord;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $requisition_id
|
||||
* @property string $number
|
||||
* @property array<string, mixed> $snapshot
|
||||
*/
|
||||
class PurchaseOrder extends Model
|
||||
{
|
||||
use BelongsToTenant, ImmutablePurchaseRecord;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['snapshot' => 'array', 'issued_at' => 'datetime'];
|
||||
}
|
||||
|
||||
/** @return HasMany<PurchaseOrderItem, $this> */
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(PurchaseOrderItem::class);
|
||||
}
|
||||
|
||||
/** @param Builder<PurchaseOrder> $query */
|
||||
public function scopeVisibleTo(Builder $query, User $user): void
|
||||
{
|
||||
$query->whereIn('requisition_id', Requisition::query()->visibleTo($user)->select('id'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use App\Models\Concerns\ImmutablePurchaseRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PurchaseOrderItem extends Model
|
||||
{
|
||||
use BelongsToTenant, ImmutablePurchaseRecord;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/** @property array<string, mixed> $weights */
|
||||
class Quotation extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['weights' => 'array', 'valid_until' => 'date:Y-m-d'];
|
||||
}
|
||||
|
||||
/** @return BelongsTo<Supplier, $this> */
|
||||
public function supplier(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Supplier::class);
|
||||
}
|
||||
|
||||
/** @return HasMany<QuotationItem, $this> */
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(QuotationItem::class);
|
||||
}
|
||||
|
||||
/** @return HasMany<ProposalEvaluation, $this> */
|
||||
public function evaluations(): HasMany
|
||||
{
|
||||
return $this->hasMany(ProposalEvaluation::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property string $quantity
|
||||
* @property string $tax_rate
|
||||
*/
|
||||
class QuotationItem extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['quantity' => 'decimal:3', 'tax_rate' => 'decimal:2'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\PurchasingRole;
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use App\Services\ConflictGuard;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $department_id
|
||||
* @property int $requester_id
|
||||
* @property string $number
|
||||
* @property string $status
|
||||
* @property int $subtotal_minor
|
||||
* @property int $tax_minor
|
||||
* @property int $total_minor
|
||||
* @property Collection<int, ApprovalStep> $steps
|
||||
* @property Collection<int, RequisitionItem> $items
|
||||
*/
|
||||
class Requisition extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['approval_snapshot' => 'array', 'submitted_at' => 'datetime', 'needed_by' => 'date:Y-m-d'];
|
||||
}
|
||||
|
||||
/** @return HasMany<RequisitionItem, $this> */
|
||||
public function items(): HasMany
|
||||
{
|
||||
return $this->hasMany(RequisitionItem::class);
|
||||
}
|
||||
|
||||
/** @return HasMany<ApprovalStep, $this> */
|
||||
public function steps(): HasMany
|
||||
{
|
||||
return $this->hasMany(ApprovalStep::class)->orderBy('position');
|
||||
}
|
||||
|
||||
/** @return BelongsTo<User, $this> */
|
||||
public function requester(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'requester_id');
|
||||
}
|
||||
|
||||
/** @return BelongsTo<Department, $this> */
|
||||
public function department(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Department::class);
|
||||
}
|
||||
|
||||
/** @return BelongsTo<CostCenter, $this> */
|
||||
public function costCenter(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CostCenter::class);
|
||||
}
|
||||
|
||||
/** @param Builder<Requisition> $query */
|
||||
public function scopeVisibleTo(Builder $query, User $user): void
|
||||
{
|
||||
match ($user->purchasing_role) {
|
||||
PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance => null,
|
||||
PurchasingRole::DepartmentHead => $query->where('department_id', $user->department_id),
|
||||
default => $query->where('requester_id', $user->id),
|
||||
};
|
||||
}
|
||||
|
||||
public function canDecide(User $user): bool
|
||||
{
|
||||
if ($this->tenant_id !== $user->tenant_id || $this->status !== 'pending' || $this->requester_id === $user->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->steps->contains('approver_id', $user->id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$next = $this->steps->firstWhere('decision', 'pending');
|
||||
|
||||
return $next !== null && $next->role === $user->purchasing_role->value
|
||||
&& ($next->role !== 'department_head' || $this->department_id === $user->department_id)
|
||||
&& ! app(ConflictGuard::class)->participationBlocked($user, $this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Decimal casts expose exact decimal strings for monetary calculations.
|
||||
*
|
||||
* @property string $quantity
|
||||
* @property string $tax_rate
|
||||
*/
|
||||
class RequisitionItem extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['quantity' => 'decimal:3', 'tax_rate' => 'decimal:2'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $tenant_id
|
||||
* @property int $created_by
|
||||
* @property int|null $last_changed_by
|
||||
* @property int $version
|
||||
*/
|
||||
class Supplier extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $attributes = ['version' => 1];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['blocked' => 'boolean', 'next_review' => 'date:Y-m-d'];
|
||||
}
|
||||
|
||||
public function eligible(): bool
|
||||
{
|
||||
return $this->status === 'active' && ! $this->blocked && $this->next_review >= today();
|
||||
}
|
||||
|
||||
/** @return HasMany<SupplierDocument, $this> */
|
||||
public function documents(): HasMany
|
||||
{
|
||||
return $this->hasMany(SupplierDocument::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToTenant;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int $supplier_id
|
||||
* @property string $path
|
||||
* @property string $original_name
|
||||
*/
|
||||
class SupplierDocument extends Model
|
||||
{
|
||||
use BelongsToTenant;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $hidden = ['path', 'tenant_id'];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property int $first_limit_minor
|
||||
* @property int $second_limit_minor
|
||||
* @property bool $approval_includes_tax
|
||||
* @property bool $rules_provisional
|
||||
*/
|
||||
class Tenant extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['approval_includes_tax' => 'boolean', 'rules_provisional' => 'boolean'];
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\PurchasingRole;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Attributes\Hidden;
|
||||
@@ -12,6 +13,9 @@ use Illuminate\Support\Carbon;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
* @property int|null $tenant_id
|
||||
* @property int|null $department_id
|
||||
* @property PurchasingRole $purchasing_role
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property Carbon|null $email_verified_at
|
||||
@@ -38,6 +42,7 @@ class User extends Authenticatable
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'purchasing_role' => PurchasingRole::class,
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user