Preserva entrega A aceite de servicos em rascunho
This commit is contained in:
@@ -9,7 +9,7 @@ final class Audit
|
||||
public static function record(string $action, ?int $actor, ?int $tenant = null, ?int $company = null, ?int $unit = null, ?int $subject = null, array $details = []): void
|
||||
{
|
||||
// Explicit allowlist: credentials, session/context tokens and request bodies never enter audit.
|
||||
$safe = array_intersect_key($details, array_flip(['before_name', 'after_name', 'permission', 'reason', 'operator', 'grants']));
|
||||
$safe = array_intersect_key($details, array_flip(['before_name', 'after_name', 'permission', 'reason', 'operator', 'grants', 'area', 'record_id', 'version', 'event_id']));
|
||||
DB::table('identity_audit')->insert([
|
||||
'action' => $action, 'actor_id' => $actor, 'tenant_id' => $tenant,
|
||||
'company_id' => $company, 'unit_id' => $unit, 'subject_id' => $subject,
|
||||
|
||||
@@ -112,6 +112,7 @@ final class IdentityWorkflow
|
||||
$members = DB::table('company_memberships as m')->join('users as u', 'u.id', '=', 'm.user_id')
|
||||
->where('m.tenant_id', $ids['tenant_id'])->where('m.company_id', $ids['company_id'])->select('u.id', 'u.name', 'u.email')->orderBy('u.name')->get();
|
||||
$grants = DB::table('permission_grants')->where('tenant_id', $ids['tenant_id'])->where('company_id', $ids['company_id'])
|
||||
->whereIn('permission', array_merge(Permissions::COMPANY, Permissions::UNIT))
|
||||
->where(fn ($q) => $q->whereNull('unit_id')->orWhereIn('unit_id', $allowedUnits))->get(['id', 'user_id', 'unit_id', 'permission']);
|
||||
return ['members' => $members, 'grants' => $grants, 'catalogue' => ['company' => Permissions::COMPANY, 'unit' => Permissions::UNIT]];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Services;
|
||||
|
||||
use Brick\Math\BigDecimal;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
final class DraftValues
|
||||
{
|
||||
public static function decimal(mixed $value, string $field, bool $positive = false): ?string
|
||||
{
|
||||
if ($value === null || $value === '') { return null; }
|
||||
if (! is_string($value) || ! preg_match('/^(0|[1-9][0-9]{0,15})(\.[0-9]{1,8})?$/D', $value)) {
|
||||
throw ValidationException::withMessages([$field => 'Use uma cadeia decimal com ponto, até 16 algarismos inteiros e 8 decimais; sem expoentes ou arredondamento.']);
|
||||
}
|
||||
$decimal = BigDecimal::of($value);
|
||||
if ($positive && $decimal->isLessThanOrEqualTo(BigDecimal::zero())) { throw ValidationException::withMessages([$field => 'A quantidade deve ser superior a zero quando preenchida.']); }
|
||||
return (string) $decimal->stripTrailingZeros();
|
||||
}
|
||||
|
||||
public static function lines(array $lines, array $scope): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($lines as $index => $line) {
|
||||
$service = DB::table('a_services')->where('tenant_id', $scope['tenant_id'])->where('company_id', $scope['company_id'])->where('id', $line['service_id'])->first();
|
||||
abort_unless($service, 403, 'Serviço fora da empresa autorizada.');
|
||||
$result[] = ['service_id' => (int) $service->id, 'service_name' => $service->name,
|
||||
'description' => $line['description'] ?? null,
|
||||
'quantity' => self::decimal($line['quantity'] ?? null, "lines.$index.quantity", true),
|
||||
'unit_price' => self::decimal($line['unit_price'] ?? null, "lines.$index.unit_price")];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function pending(array $data): array
|
||||
{
|
||||
$pending = ['tax_rule_pending' => 'Imposto por definir e validar; não foi considerado zero.',
|
||||
'accounting_rule_pending' => 'Regras contabilísticas por validar; nenhum lançamento foi produzido.'];
|
||||
if (empty($data['currency'])) { $pending['currency_missing'] = 'Moeda não indicada; nenhuma moeda foi presumida.'; }
|
||||
else { $pending['currency_validation_pending'] = 'Moeda indicada pelo utilizador; a sua aplicabilidade e a moeda funcional continuam por validar.'; }
|
||||
foreach ($data['lines'] as $index => $line) {
|
||||
if ($line['quantity'] === null) { $pending["quantity_missing_$index"] = 'Quantidade por preencher na linha '.($index + 1).'.'; }
|
||||
if ($line['unit_price'] === null) { $pending["price_missing_$index"] = 'Preço proposto por preencher na linha '.($index + 1).'.'; }
|
||||
}
|
||||
return $pending;
|
||||
}
|
||||
|
||||
public static function base(array $data): ?string
|
||||
{
|
||||
$sum = BigDecimal::zero();
|
||||
foreach ($data['lines'] ?? [] as $line) {
|
||||
if ($line['quantity'] === null || $line['unit_price'] === null) { return null; }
|
||||
$sum = $sum->plus(BigDecimal::of($line['quantity'])->multipliedBy($line['unit_price']));
|
||||
}
|
||||
return (string) $sum->stripTrailingZeros();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Services\Http;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Modules\Services\ServiceWorkflow;
|
||||
use App\Modules\Services\ServiceAccess;
|
||||
use App\Modules\Services\ServicePermissions;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
final class ServiceController extends Controller
|
||||
{
|
||||
public function index(Request $request, string $area, ServiceWorkflow $workflow) { return response()->json(['records' => $workflow->list($area, $request->user()->id)]); }
|
||||
public function show(Request $request, string $area, int $id, ServiceWorkflow $workflow) { return response()->json($workflow->show($area, $id, $request->user()->id)); }
|
||||
public function create(Request $request, string $area, ServiceWorkflow $workflow) { return $this->save($request, $area, null, $workflow); }
|
||||
public function update(Request $request, string $area, int $id, ServiceWorkflow $workflow) { return $this->save($request, $area, $id, $workflow); }
|
||||
|
||||
private function save(Request $request, string $area, ?int $id, ServiceWorkflow $workflow)
|
||||
{
|
||||
abort_unless(in_array($area, ServicePermissions::AREAS, true), 404);
|
||||
$rules = ['name' => ServicePermissions::unitScoped($area) ? 'sometimes|required|string|max:255' : 'required|string|max:255',
|
||||
'description' => 'nullable|string|max:2000', 'version' => $id ? 'required|integer|min:1' : 'prohibited'];
|
||||
if ($area === 'clients') { unset($rules['description']); }
|
||||
if (ServicePermissions::unitScoped($area)) {
|
||||
$rules += ['currency' => 'nullable|string|regex:/^[A-Z]{3}$/', 'lines' => $area === 'service_drafts' ? 'required|array|min:1|max:100' : 'sometimes|array|min:1|max:100',
|
||||
'lines.*' => 'array:service_id,service_name,description,quantity,unit_price', 'lines.*.service_id' => 'required|integer|min:1',
|
||||
'lines.*.service_name' => 'sometimes|string|max:255', 'lines.*.description' => 'nullable|string|max:2000', 'lines.*.quantity' => 'nullable|string|max:30', 'lines.*.unit_price' => 'nullable|string|max:30'];
|
||||
if ($area === 'service_drafts') { $rules['client_id'] = 'required|integer|min:1'; }
|
||||
else { $rules['source_id'] = $id ? 'prohibited' : 'required|integer|min:1'; $rules['reload_source'] = 'sometimes|boolean'; }
|
||||
}
|
||||
$unknown = array_diff(array_keys($request->all()), array_keys($rules));
|
||||
abort_if($unknown !== [], 422, 'Campo não suportado ou identificador protegido.');
|
||||
$data = $request->validate($rules);
|
||||
$key = (string) $request->header('Idempotency-Key', '');
|
||||
return response()->json($workflow->save($area, $id, $request->user()->id, $data, $key), $id ? 200 : 201);
|
||||
}
|
||||
|
||||
public function accesses(Request $request, string $area, ServiceAccess $access) { return response()->json($access->listing($area, $request->user()->id)); }
|
||||
public function changeAccess(Request $request, string $area, ServiceAccess $access)
|
||||
{
|
||||
$data = $request->validate(['email' => 'required|email|max:255', 'action' => 'required|string', 'grant' => 'required|boolean', 'reason' => 'required|string|max:500']);
|
||||
abort_if(array_diff(array_keys($request->all()), ['email','action','grant','reason']) !== [], 422);
|
||||
$access->change($area, $request->user()->id, $data);
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Services;
|
||||
|
||||
use App\Modules\Identity\Services\Audit;
|
||||
use App\Modules\Identity\Services\Permissions;
|
||||
use App\Shared\Context\CompanyContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class ServiceAccess
|
||||
{
|
||||
public function __construct(private ServiceWorkflow $workflow, private CompanyContext $context, private Permissions $permissions) {}
|
||||
|
||||
public function listing(string $area, int $actor): array
|
||||
{
|
||||
$scope = $this->workflow->scope($area, $actor, 'manage');
|
||||
$grants = DB::table('permission_grants as g')->join('users as u', 'u.id', '=', 'g.user_id')->where('g.tenant_id', $scope['tenant_id'])->where('g.company_id', $scope['company_id'])->where('g.unit_id', $scope['unit_id'])->whereIn('g.permission', array_map(fn ($action) => "$area.$action", ServicePermissions::ACTIONS))
|
||||
->orderBy('u.name')->get(['g.id','g.permission','g.unit_id','u.name','u.email']);
|
||||
$actions = array_values(array_filter(ServicePermissions::ACTIONS, fn ($action) => $this->permissions->has($actor, $scope['tenant_id'], $scope['company_id'], "$area.$action", $scope['unit_id'])));
|
||||
return ['grants' => $grants, 'actions' => $actions, 'scope' => $scope];
|
||||
}
|
||||
|
||||
public function change(string $area, int $actor, array $input): void
|
||||
{
|
||||
$scope = $this->workflow->scope($area, $actor, 'manage'); $selected = $this->context->unitId();
|
||||
DB::transaction(function () use ($area, $actor, $input, $scope, $selected): void {
|
||||
DB::table('companies')->where('id', $scope['company_id'])->lockForUpdate()->firstOrFail();
|
||||
$this->context->selectFor($actor, $scope['tenant_id'], $scope['company_id'], $selected);
|
||||
$this->workflow->scope($area, $actor, 'manage');
|
||||
$permission = "$area.".$input['action'];
|
||||
abort_unless(in_array($input['action'], ServicePermissions::ACTIONS, true), 422);
|
||||
$this->permissions->require($actor, $scope['tenant_id'], $scope['company_id'], $permission, $scope['unit_id']);
|
||||
$target = DB::table('users')->where('email', mb_strtolower($input['email']))->first(['id']);
|
||||
abort_unless($target, 422, 'Utilizador inexistente; nenhuma conta é criada por concessão.');
|
||||
$this->permissions->require($target->id, $scope['tenant_id'], $scope['company_id'], 'company.view');
|
||||
if ($scope['unit_id']) { $this->permissions->require($target->id, $scope['tenant_id'], $scope['company_id'], 'unit.view', $scope['unit_id']); }
|
||||
$query = DB::table('permission_grants')->where('tenant_id', $scope['tenant_id'])->where('company_id', $scope['company_id'])->where('unit_id', $scope['unit_id'])->where('user_id', $target->id);
|
||||
if ($input['grant']) {
|
||||
if ($input['action'] !== 'view') { $this->permissions->require($target->id, $scope['tenant_id'], $scope['company_id'], "$area.view", $scope['unit_id']); }
|
||||
DB::table('permission_grants')->insertOrIgnore($scope + ['user_id' => $target->id, 'permission' => $permission, 'created_at' => now(), 'updated_at' => now()]);
|
||||
$changed = [['permission' => $permission, 'unit_id' => $scope['unit_id']]];
|
||||
} else {
|
||||
$query->whereIn('permission', $input['action'] === 'view' ? array_map(fn ($action) => "$area.$action", ServicePermissions::ACTIONS) : [$permission]);
|
||||
$changed = $query->get(['permission','unit_id'])->map(fn ($row) => (array) $row)->all();
|
||||
foreach ($changed as $row) { $this->permissions->require($actor, $scope['tenant_id'], $scope['company_id'], $row['permission'], $scope['unit_id']); }
|
||||
$query->delete();
|
||||
}
|
||||
Audit::record($input['grant'] ? 'a.access.granted' : 'a.access.revoked', $actor, $scope['tenant_id'], $scope['company_id'], $scope['unit_id'], $target->id,
|
||||
['permission' => $permission, 'reason' => $input['reason'], 'grants' => $changed]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Services;
|
||||
|
||||
final class ServicePermissions
|
||||
{
|
||||
public const AREAS = ['clients', 'services', 'service_drafts', 'billing_drafts'];
|
||||
public const ACTIONS = ['view', 'create', 'edit', 'manage'];
|
||||
public const LABELS = ['clients' => 'Clientes', 'services' => 'Serviços', 'service_drafts' => 'Prestações em rascunho', 'billing_drafts' => 'Rascunhos de facturação'];
|
||||
|
||||
public static function unitScoped(string $area): bool { return in_array($area, ['service_drafts', 'billing_drafts'], true); }
|
||||
public static function catalogue(bool $unit): array
|
||||
{
|
||||
$result = [];
|
||||
foreach (self::AREAS as $area) {
|
||||
if (self::unitScoped($area) === $unit) {
|
||||
foreach (self::ACTIONS as $action) { $result[] = "$area.$action"; }
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Services;
|
||||
|
||||
use App\Modules\Identity\Services\Audit;
|
||||
use App\Modules\Identity\Services\Permissions;
|
||||
use App\Shared\Context\CompanyContext;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class ServiceWorkflow
|
||||
{
|
||||
public function __construct(private CompanyContext $context, private Permissions $permissions) {}
|
||||
|
||||
public function scope(string $area, int $actor, string $action): array
|
||||
{
|
||||
abort_unless(in_array($area, ServicePermissions::AREAS, true) && in_array($action, ServicePermissions::ACTIONS, true), 404);
|
||||
$ids = $this->context->identifiers();
|
||||
abort_unless($this->context->userId() === $actor, 403);
|
||||
$unit = ServicePermissions::unitScoped($area) ? $this->context->unitId() : null;
|
||||
abort_if(ServicePermissions::unitScoped($area) && $unit === null, 403, 'Seleccione uma unidade autorizada para esta área.');
|
||||
$this->permissions->require($actor, $ids['tenant_id'], $ids['company_id'], "$area.view", $unit);
|
||||
$this->permissions->require($actor, $ids['tenant_id'], $ids['company_id'], "$area.$action", $unit);
|
||||
return $ids + ['unit_id' => $unit];
|
||||
}
|
||||
|
||||
private function query(string $area, array $scope)
|
||||
{
|
||||
return DB::table('a_'.$area)->where('tenant_id', $scope['tenant_id'])->where('company_id', $scope['company_id'])->where('unit_id', $scope['unit_id']);
|
||||
}
|
||||
|
||||
public function list(string $area, int $actor): array
|
||||
{
|
||||
$scope = $this->scope($area, $actor, 'view');
|
||||
return $this->query($area, $scope)->orderByDesc('id')->get()->map(fn ($item) => $this->describe($area, $item))->all();
|
||||
}
|
||||
|
||||
public function show(string $area, int $id, int $actor): array
|
||||
{
|
||||
$scope = $this->scope($area, $actor, 'view');
|
||||
$record = $this->query($area, $scope)->where('id', $id)->first();
|
||||
abort_unless($record, 403, 'Registo fora do âmbito autorizado.');
|
||||
$result = $this->describe($area, $record);
|
||||
$result['history'] = DB::table('a_history')->where('area', $area)->where('record_id', $id)->orderBy('version')
|
||||
->get(['id','version','actor_id','snapshot','created_at'])->map(function ($row) {
|
||||
$row->snapshot = json_decode($row->snapshot, true);
|
||||
$row->event = DB::table('a_events as e')->join('a_outbox as o', 'o.event_id', '=', 'e.id')->where('e.history_id', $row->id)
|
||||
->first(['e.id','e.type','e.schema_version','o.state']);
|
||||
return $row;
|
||||
})->all();
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function describe(string $area, object $row): array
|
||||
{
|
||||
$result = (array) $row;
|
||||
$result['data'] = json_decode($row->data, true, 32, JSON_THROW_ON_ERROR);
|
||||
$result['version'] = (int) $row->version;
|
||||
$result['base_amount'] = ServicePermissions::unitScoped($area) ? DraftValues::base($result['data']) : null;
|
||||
$history = DB::table('a_history')->where('area', $area)->where('record_id', $row->id)->where('version', $row->version)->value('id');
|
||||
$result['pending'] = $history ? DB::table('a_pending')->where('history_id', $history)->pluck('message', 'code')->all() : [];
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function save(string $area, ?int $id, int $actor, array $input, string $key): array
|
||||
{
|
||||
$action = $id === null ? 'create' : 'edit';
|
||||
$scope = $this->scope($area, $actor, $action);
|
||||
$selectedUnit = $this->context->unitId();
|
||||
abort_unless((bool) preg_match('/^[a-zA-Z0-9-]{8,100}$/D', $key), 422, 'É necessária uma chave de repetição válida.');
|
||||
$hash = hash('sha256', json_encode($this->canonical([$area, $id, $scope, $input]), JSON_THROW_ON_ERROR));
|
||||
return DB::transaction(function () use ($area, $id, $actor, $input, $key, $scope, $selectedUnit, $hash, $action): array {
|
||||
DB::table('companies')->where('id', $scope['company_id'])->where('tenant_id', $scope['tenant_id'])->lockForUpdate()->firstOrFail();
|
||||
$this->context->selectFor($actor, $scope['tenant_id'], $scope['company_id'], $selectedUnit);
|
||||
$this->scope($area, $actor, $action);
|
||||
$previous = DB::table('a_commands')->where('tenant_id', $scope['tenant_id'])->where('company_id', $scope['company_id'])->where('command_key', $key)->first();
|
||||
if ($previous) {
|
||||
abort_unless((int) $previous->actor_id === $actor && hash_equals($previous->request_hash, $hash), 409, 'Chave repetida com conteúdo ou autor diferente.');
|
||||
return json_decode($previous->result, true, 32, JSON_THROW_ON_ERROR) + ['replayed' => true];
|
||||
}
|
||||
$old = $id ? $this->query($area, $scope)->where('id', $id)->first() : null;
|
||||
if ($id) {
|
||||
abort_unless($old, 403, 'Registo fora do âmbito autorizado.');
|
||||
abort_unless((int) $old->version === (int) ($input['version'] ?? 0), 409, 'O registo mudou. Reabra-o antes de guardar.');
|
||||
}
|
||||
$version = $old ? (int) $old->version + 1 : 1;
|
||||
$values = $this->normalise($area, $input, $scope, $old);
|
||||
$data = $values['data']; $values['data'] = json_encode($data === [] ? new \stdClass() : $data, JSON_THROW_ON_ERROR);
|
||||
$values += ['version' => $version, 'updated_at' => now()];
|
||||
if ($id) { $this->query($area, $scope)->where('id', $id)->update($values); }
|
||||
else { $id = DB::table('a_'.$area)->insertGetId($scope + $values + ['created_at' => now()]); }
|
||||
$record = $this->query($area, $scope)->where('id', $id)->first();
|
||||
$snapshot = (array) $record; $snapshot['data'] = $data;
|
||||
$pending = ServicePermissions::unitScoped($area) ? DraftValues::pending($data) : [];
|
||||
if ($area === 'billing_drafts') {
|
||||
$current = DB::table('a_service_drafts')->where('id', $record->source_id)->value('version');
|
||||
if ((int) $current !== (int) $data['source_version']) { $pending['source_changed'] = 'A prestação tem uma versão mais recente. Reveja ou actualize explicitamente os dados de origem.'; }
|
||||
}
|
||||
$snapshot['pending'] = $pending;
|
||||
$history = DB::table('a_history')->insertGetId($scope + ['area' => $area, 'record_id' => $id, 'version' => $version,
|
||||
'actor_id' => $actor, 'snapshot' => json_encode($snapshot, JSON_THROW_ON_ERROR), 'created_at' => now()]);
|
||||
foreach ($pending as $code => $message) { DB::table('a_pending')->insert(['history_id' => $history, 'code' => $code, 'message' => $message, 'created_at' => now()]); }
|
||||
$affected = [];
|
||||
if ($area === 'service_drafts' && $old) {
|
||||
foreach (DB::table('a_billing_drafts')->where('source_id', $id)->where('tenant_id', $scope['tenant_id'])->where('company_id', $scope['company_id'])->where('unit_id', $scope['unit_id'])->get(['id','version']) as $billing) {
|
||||
$billingHistory = DB::table('a_history')->where('area', 'billing_drafts')->where('record_id', $billing->id)->where('version', $billing->version)->value('id');
|
||||
DB::table('a_pending')->insertOrIgnore(['history_id' => $billingHistory, 'code' => 'source_changed', 'message' => 'A prestação de origem foi alterada; os dados deste rascunho não foram reescritos.', 'created_at' => now()]);
|
||||
$affected[] = $billing->id;
|
||||
}
|
||||
}
|
||||
$event = DB::table('a_events')->insertGetId(['history_id' => $history, 'type' => "a.$area.".($old ? 'updated' : 'created'),
|
||||
'schema_version' => 1, 'payload' => json_encode(['snapshot' => $snapshot, 'affected_billing_drafts' => $affected], JSON_THROW_ON_ERROR), 'created_at' => now()]);
|
||||
DB::table('a_outbox')->insert(['event_id' => $event, 'state' => 'captured', 'created_at' => now()]);
|
||||
Audit::record("a.$area.".($old ? 'updated' : 'created'), $actor, $scope['tenant_id'], $scope['company_id'], $scope['unit_id'],
|
||||
details: ['area' => $area, 'record_id' => $id, 'version' => $version, 'event_id' => $event]);
|
||||
$result = ['id' => $id, 'version' => $version, 'event_id' => $event];
|
||||
DB::table('a_commands')->insert(['tenant_id' => $scope['tenant_id'], 'company_id' => $scope['company_id'], 'actor_id' => $actor,
|
||||
'command_key' => $key, 'request_hash' => $hash, 'result' => json_encode($result, JSON_THROW_ON_ERROR), 'created_at' => now()]);
|
||||
return $result + ['replayed' => false];
|
||||
});
|
||||
}
|
||||
|
||||
private function normalise(string $area, array $input, array $scope, ?object $old): array
|
||||
{
|
||||
if (! ServicePermissions::unitScoped($area)) {
|
||||
return ['name' => trim($input['name']), 'data' => $area === 'clients' ? [] : ['description' => $input['description'] ?? null]];
|
||||
}
|
||||
$oldData = $old ? json_decode($old->data, true) : [];
|
||||
$actor = $this->context->userId();
|
||||
foreach (['clients.view', 'services.view'] as $permission) { $this->permissions->require($actor, $scope['tenant_id'], $scope['company_id'], $permission); }
|
||||
$source = null;
|
||||
if ($area === 'billing_drafts') {
|
||||
$sourceId = $old ? $old->source_id : $input['source_id'];
|
||||
$source = DB::table('a_service_drafts')->where('tenant_id', $scope['tenant_id'])->where('company_id', $scope['company_id'])->where('unit_id', $scope['unit_id'])->where('id', $sourceId)->first();
|
||||
abort_unless($source, 403, 'Prestação fora da unidade autorizada.');
|
||||
$reload = ! $old || ($input['reload_source'] ?? false);
|
||||
if ($reload) { $this->permissions->require($actor, $scope['tenant_id'], $scope['company_id'], 'service_drafts.view', $scope['unit_id']); }
|
||||
$base = $reload ? json_decode($source->data, true) : $oldData;
|
||||
$clientId = $reload ? $source->client_id : $old->client_id;
|
||||
$lines = $input['lines'] ?? $base['lines'];
|
||||
$allowedIds = array_column($base['lines'], 'service_id');
|
||||
foreach ($lines as $line) { abort_unless(in_array((int) $line['service_id'], $allowedIds, true), 422, 'Use os serviços da prestação de origem.'); }
|
||||
$currency = array_key_exists('currency', $input) ? $input['currency'] : ($base['currency'] ?? null);
|
||||
$sourceVersion = $reload ? (int) $source->version : (int) $oldData['source_version'];
|
||||
} else {
|
||||
$clientId = $input['client_id']; $lines = $input['lines']; $currency = $input['currency'] ?? null;
|
||||
}
|
||||
$client = DB::table('a_clients')->where('tenant_id', $scope['tenant_id'])->where('company_id', $scope['company_id'])->where('id', $clientId)->first();
|
||||
abort_unless($client, 403, 'Cliente fora da empresa autorizada.');
|
||||
$data = ['description' => $input['description'] ?? ($base['description'] ?? ($oldData['description'] ?? null)), 'currency' => $currency,
|
||||
'lines' => DraftValues::lines($lines, $scope), 'client_name' => $client->name, 'tax_status' => 'pending', 'accounting_status' => 'not_processed'];
|
||||
$result = ['client_id' => (int) $clientId, 'state' => 'draft', 'data' => $data,
|
||||
'name' => trim($input['name'] ?? ($old?->name ?? ($area === 'billing_drafts' ? 'Rascunho da prestação #'.$source->id : 'Prestação de '.$client->name)))];
|
||||
if ($source) { $result['source_id'] = $source->id; $result['data']['source_version'] = $sourceVersion; }
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function canonical(mixed $value): mixed
|
||||
{
|
||||
if (! is_array($value)) { return $value; }
|
||||
if (! array_is_list($value)) { ksort($value); }
|
||||
return array_map(fn ($entry) => $this->canonical($entry), $value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user