Mostrar oportunidades publicadas antes do registo de candidatos

This commit is contained in:
2026-10-30 21:15:27 +02:00
parent bf84575d10
commit c22846b2b1
10 changed files with 528 additions and 8 deletions
@@ -48,9 +48,11 @@ class CandidatePortalController extends Controller
$portal = $profiles->portal($company);
abort_unless($portal !== null, 404);
$tenant = Tenant::query()->findOrFail($company);
$filter = $request->validate(['opportunities_page' => 'sometimes|integer|min:1']);
return $this->page($request, 'candidate/entry', ['company' => ['id' => $company, 'name' => $tenant->name],
'portal' => ['version' => $portal->version, 'registration_open' => (bool) $portal->registration_open, 'notice' => $portal->notice]]);
'portal' => ['version' => $portal->version, 'registration_open' => (bool) $portal->registration_open, 'notice' => $portal->notice],
'opportunities' => app(CandidateSubmissions::class)->publicOpportunities($company, (int) ($filter['opportunities_page'] ?? 1))]);
}
public function register(Request $request, int $company, CandidateProfiles $profiles): Response
@@ -90,13 +92,12 @@ class CandidatePortalController extends Controller
return redirect('/candidaturas/'.$company);
}
$profiles->event($account, 'profile.consulted', []);
$filter = $request->validate(['opportunities_page' => 'sometimes|integer|min:1']);
$opportunities = app(CandidateSubmissions::class)->publicOpportunities($company, (int) ($filter['opportunities_page'] ?? 1))->withPath('/candidaturas/'.$company.'/perfil');
return $this->page($request, 'candidate/profile', ['companyId' => $company, 'email' => $account->email, 'profile' => $profiles->profile($account),
'emailVerified' => $account->email_verified_at !== null, 'accessStatus' => $request->session()->get('candidate_access_status'), 'emailDeliveryAvailable' => app(CandidateAccess::class)->deliveryAvailable(),
'opportunities' => Schema::hasTable('candidate_opportunity_publications') ? DB::table('candidate_opportunity_publications as p')->join('people_processes as r', 'r.id', '=', 'p.people_process_id')
->where('p.tenant_id', $company)->where('r.tenant_id', $company)->where('p.active', true)->where('r.status', 'open')->where('r.closes_on', '>=', today()->toDateString())
->whereNotExists(fn ($q) => $q->selectRaw('1')->from('candidate_opportunity_publications as newer')->whereColumn('newer.people_process_id', 'p.people_process_id')->whereColumn('newer.version', '>', 'p.version'))
->orderBy('r.closes_on')->limit(20)->get(['r.id', 'r.title', 'r.closes_on']) : [],
'opportunities' => $opportunities->items(), 'opportunityPagination' => ['prev' => $opportunities->previousPageUrl(), 'next' => $opportunities->nextPageUrl()],
'submissions' => Schema::hasTable('candidate_submissions') ? DB::table('candidate_submissions')->where('tenant_id', $company)->where('candidate_account_id', $account->id)->latest('id')->limit(20)->get(['id', 'reference', 'created_at']) : []]);
}
+20
View File
@@ -5,6 +5,7 @@ namespace App\Services;
use App\Models\CandidateAccount;
use App\Models\PeopleProcess;
use App\Models\User;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
@@ -15,6 +16,25 @@ use stdClass;
class CandidateSubmissions
{
/** Only explicitly published, current and open opportunities; no internal process fields.
* @return LengthAwarePaginator<int, stdClass>
*/
public function publicOpportunities(int $company, int $page = 1): LengthAwarePaginator
{
$path = '/candidaturas/'.$company;
if (! Schema::hasTable('candidate_opportunity_publications')) {
return (new LengthAwarePaginator([], 0, 12, $page))->withPath($path)->setPageName('opportunities_page');
}
return DB::table('candidate_opportunity_publications as p')->join('people_processes as r', 'r.id', '=', 'p.people_process_id')
->where('p.tenant_id', $company)->where('r.tenant_id', $company)->where('p.active', true)
->where('r.status', 'open')->where('r.closes_on', '>=', today()->toDateString())
->whereNotExists(fn ($q) => $q->selectRaw('1')->from('candidate_opportunity_publications as newer')
->whereColumn('newer.people_process_id', 'p.people_process_id')->whereColumn('newer.tenant_id', 'p.tenant_id')->whereColumn('newer.version', '>', 'p.version'))
->orderBy('r.closes_on')->orderBy('r.id')
->paginate(12, ['r.id', 'r.title', 'r.kind', 'r.closes_on', 'p.description'], 'opportunities_page', $page)->withPath($path);
}
/** @param array<string,mixed> $input */
public function publish(User $actor, int $processId, array $input): void
{
+20
View File
@@ -1,5 +1,25 @@
# Desenvolvimento do SGC
## Consulta de oportunidades antes do registo, 30 de Outubro de 2026
A entrada da área do candidato mostra oportunidades explicitamente publicadas, abertas e dentro do prazo, com título, tipo, data e descrição pública. Não expõe finalidade interna, regras, candidaturas, avaliações ou documentos. A consulta é paginada por empresa; a última versão da publicação controla a visibilidade, incluindo retirada. O fecho de novos registos mantém a consulta pública e a entrada das contas existentes. Preparar e enviar candidaturas continuam a exigir autenticação própria, perfil e confirmação de email conforme as regras já existentes.
A área autenticada reutiliza a mesma consulta e passa a ter paginação, em vez de omitir oportunidades depois das primeiras 20. Retirada a mensagem desactualizada de que o envio de documentos ainda não estava disponível. Não foram abertas inscrições, publicadas oportunidades ou criadas contas para testar no domínio.
Verificação dirigida: 24 testes PHP/374 asserções em perfil e submissões; PostgreSQL descartável: 64 testes/1401 asserções, incluindo Pessoas e formatos de documentos. Dez testes frontend dirigidos, tipos, PHPStan sem erros, Pint e lint dirigidos aprovados. As provas integrais aceites da entrega bf84575 são reutilizadas, sem alegar nova execução de toda a suíte. Conferência visual tentada novamente: conector devolveu native pipe indisponível e browser host indisponível. Este incremento continua local e não faz parte da release bf84575 activa no domínio. Provas `proofs/candidate-public-opportunities.json` e `candidate-public-opportunities-pg.json`.
## Entrega em produção confirmada — bf84575, 30 de Outubro de 2026
O commit `bf84575d10d9063c2224945e6d5627da826148a9` está em main e activo no domínio, na release `20261030-190744-bf84575d10d9-KyKKDI71`. O serviço existente terminou com Result=success e ExecMainStatus=0. Esta confirmação actualiza as indicações históricas de instalação apenas local abaixo.
O instalador foi corrigido para exigir HTTP 200 em `/dashboard`: o anterior teste da raiz recebia o redireccionamento público esperado e provocava rollback. A validação e o rollback do instalador foram mantidos. Configuração anterior salvaguardada antes da correcção.
Delta reconciliado: 14 migrações, de 000028 a 000041, sem repetir as 30 já registadas. Backup fresco da base e armazenamento, restauro conferido e ensaio das migrações na cópia antes da transacção de produção. As 81 tabelas anteriores conservaram os seus dados nas colunas de origem; 18 novas tabelas conferidas vazias e schema final igual ao ensaio. Existem agora 44 migrações registadas. A migração 000028 acrescenta o parâmetro de IVA por empresa com valor inicial 16%, sem reescrever os valores e snapshots anteriores. Não foram criadas contas, abertas inscrições ou publicadas oportunidades automaticamente.
Armazenamento privado em storage partilhado persistente, com escrita conferida para www-data. Limites específicos do domínio: ficheiro 50M, pedido PHP e Nginx 104M, até 20 ficheiros; configuração Nginx validada e recarregada. Os 2M/8M ainda apresentados pelo PHP CLI são os limites do terminal e não os parâmetros específicos do domínio. PHP 8.4 dispõe de DOM/ZIP/fileinfo; PHP 8.5 CLI continua sem DOM/ZIP e não foi usado na instalação.
Conferência HTTP: 21 ecrãs públicos com componentes esperados, utilizador nulo e âmbito vazio; sete percursos privados protegidos. Prova `proofs/candidate-documents-production.json`. Conferência visual dirigida e ensaio com candidatura real no domínio continuam pendentes; não foram enviados ficheiros pessoais ou emails reais para os simular.
## CV e documentos na candidatura electrónica, 30 de Outubro de 2026
Recepção de documentos integrada no mesmo envio da candidatura. A obrigatoriedade, formatos (PDF, DOCX, JPG e PNG) e limite por ficheiro vêm do formulário preservado na preparação do processo, sem procurar a versão actual da empresa. Recusados documentos desconhecidos, obrigatórios em falta, tamanho excessivo e divergência de extensão/MIME/conteúdo. DOCX exige estrutura Word/XML válida, sem declarações de entidades/DOCTYPE, macros ou executáveis conhecidos, com limites de entradas e expansão do arquivo. A conferência de formato não constitui antivírus, prova de autenticidade, validação do mérito ou aprovação documental.
@@ -0,0 +1,253 @@
{
"checked_at": "2026-10-30T19:09:50.960374+00:00",
"commit": "bf84575d10d9063c2224945e6d5627da826148a9",
"release": "/var/www/compras-deploy/releases/20261030-190744-bf84575d10d9-KyKKDI71",
"deployment_service": "success",
"migration_count": 44,
"migrations_applied": 14,
"existing_tables_preserved": 81,
"new_tables_verified_empty": 18,
"backup": "/root/compras-release-backups/20261030T190740921286Z-bf84575",
"database_restore_verified": true,
"storage_restore_verified": true,
"private_storage_shared": true,
"web_user_can_write_private_uploads": true,
"nginx_validation_ok": true,
"site_upload_configuration": [
"\n client_max_body_size 104M;",
" fastcgi_param PHP_VALUE \"upload_max_filesize=50M\\npost_max_size=104M\\nmax_file_uploads=20\";"
],
"cli_limits_apply_to_cli_only": true,
"application": {
"database_driver": "pgsql",
"migration_count": 44,
"candidate_migrations": [
"2026_10_30_000037_create_candidate_profiles",
"2026_10_30_000038_create_candidate_access_tokens",
"2026_10_30_000039_create_candidate_submissions",
"2026_10_30_000040_create_candidate_triage",
"2026_10_30_000041_create_candidate_submission_documents"
],
"documents_table_exists": true,
"document_count": 0,
"public_navigation_enabled": true,
"candidate_disk_root": "/var/www/compras-deploy/releases/20261030-190744-bf84575d10d9-KyKKDI71/storage/app/candidate-documents"
},
"http": {
"checked_at": "2026-10-30T19:08:35.710090+00:00",
"results": [
{
"path": "/",
"status": 302,
"location": "https://compras.tempo.co.mz/dashboard"
},
{
"path": "/dashboard",
"status": 200,
"component": "dashboard",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/requisicoes",
"status": 200,
"component": "requisitions/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/requisicoes/criar",
"status": 200,
"component": "requisitions/form",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/fornecedores",
"status": 200,
"component": "suppliers/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/cotacoes",
"status": 200,
"component": "quotations/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/ordens",
"status": 200,
"component": "purchases/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/recepcoes",
"status": 200,
"component": "receipts/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/stock",
"status": 200,
"component": "stock/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/stock/catalogo",
"status": 200,
"component": "stock/catalogue",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/stock/operacoes",
"status": 200,
"component": "stock/operations",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/equipamentos",
"status": 200,
"component": "equipment/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/planeamento",
"status": 200,
"component": "planning/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/pessoas",
"status": 200,
"component": "people/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/orcamentos",
"status": 200,
"component": "budgets/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/facturas",
"status": 200,
"component": "invoices/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/pagamentos",
"status": 200,
"component": "payments/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/regras-investimento",
"status": 200,
"component": "investment/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/politica-admissao",
"status": 200,
"component": "purchases/admission-policy",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/politica-recomendacao",
"status": 200,
"component": "purchases/recommendation-policy",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/revisao-compras",
"status": 200,
"component": "purchase-controls/index",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/settings/appearance",
"status": 200,
"component": "settings/appearance",
"anonymous": true,
"data_scope": "none",
"version": "d57b6033713dbe71fe3664abe9f56f4a"
},
{
"path": "/pessoas/cadastro",
"status": 302,
"location": "https://compras.tempo.co.mz/login"
},
{
"path": "/pessoas/1",
"status": 302,
"location": "https://compras.tempo.co.mz/login"
},
{
"path": "/fornecedores/1/dados-bancarios",
"status": 302,
"location": "https://compras.tempo.co.mz/login"
},
{
"path": "/administracao/parametrizacao",
"status": 302,
"location": "https://compras.tempo.co.mz/login"
},
{
"path": "/relatorios",
"status": 302,
"location": "https://compras.tempo.co.mz/login"
},
{
"path": "/settings/profile",
"status": 302,
"location": "https://compras.tempo.co.mz/login"
},
{
"path": "/candidaturas/1/submissoes/1/documentos/cv",
"status": 401
}
],
"public_pages_passed": 21,
"private_routes_protected": 7,
"server_activation_proof": "pending"
},
"visual_review": "pending",
"real_candidate_submission_in_production": "not performed"
}
@@ -0,0 +1,16 @@
{
"checked_at": "2026-10-30T19:14:38+00:00",
"database": "disposable PostgreSQL",
"application_role_reused_without_privilege_change": true,
"result": "passed",
"test_files": [
"CandidateSubmissionsTest",
"PeopleHiringTest",
"CandidatePortalTest",
"CandidateDocumentFormatsTest"
],
"original_evaluation_untouched": true,
"database_removed": true,
"tests": 64,
"assertions": 1401
}
@@ -0,0 +1,44 @@
{
"checked_at": "2026-10-30T19:15:14.138147+00:00",
"base_commit": "bf84575d10d9063c2224945e6d5627da826148a9",
"scope": [
"public published opportunity discovery before candidate registration",
"current publication deadline company and process scope",
"public content minimisation",
"shared public and authenticated pagination",
"authenticated application and document guards preserved"
],
"module_sqlite": {
"tests": 24,
"assertions": 374,
"passed": true
},
"postgresql": {
"tests": 64,
"assertions": 1401,
"passed": true,
"disposable_database_removed": true
},
"frontend": {
"tests": 10,
"passed": true
},
"typescript": true,
"phpstan_errors": 0,
"pint": true,
"targeted_lint": true,
"build": {
"exit_code": 0,
"completed": true
},
"full_suite": "reuse accepted bf84575 suite; not repeated",
"new_migrations": false,
"production_installed": false,
"visual_review": "blocked by unavailable native pipe and browser host",
"source_sha256": {
"app/Services/CandidateSubmissions.php": "63bebd4ccb04e9bc2ab19b0701567f082831814498e83cd7e9755ba2f550f6e4",
"app/Http/Controllers/CandidatePortalController.php": "e37a7df5933da0d9635e91180a027d1d00d5dd3d16efc15c8c36a3ad906f724d",
"resources/js/pages/candidate/entry.tsx": "1fb6c2f3ecd7153fe677c7c8164867df22f0cdc146c0bc32d6c82a0b61cd354e",
"resources/js/pages/candidate/profile.tsx": "3857fec4bb8c10164a13b710f95df916707ebb8e214a05117f1f530b0b7b34c5"
}
}
+76 -3
View File
@@ -5,9 +5,21 @@ import { Button } from '@/components/ui/button';
export default function Entry({
company,
portal,
opportunities = { data: [], prev_page_url: null, next_page_url: null },
}: {
company: { id: number; name: string };
portal: { version: number; registration_open: boolean; notice: string };
opportunities?: {
data: {
id: number;
title: string;
kind: string;
closes_on: string;
description: string;
}[];
prev_page_url: string | null;
next_page_url: string | null;
};
}) {
const login = useForm({ email: '', password: '' });
const register = useForm({
@@ -61,10 +73,71 @@ export default function Entry({
</h1>
<p>
Prepare o seu perfil e candidate-se às oportunidades
publicadas. O envio de anexos ainda está em desenvolvimento.
Criar um perfil não constitui candidatura ou contratação.
publicadas. Os documentos exigidos são indicados em cada
formulário. Criar um perfil não constitui candidatura ou
contratação.
</p>
<section className="space-y-3">
<section
className="space-y-3"
aria-label="Oportunidades publicadas"
>
<h2 className="text-xl font-semibold">
Oportunidades publicadas
</h2>
{opportunities.data.length === 0 && (
<p>Não existem oportunidades nesta página.</p>
)}
{opportunities.data.map((item) => (
<article
key={item.id}
className="space-y-2 rounded-lg border p-4"
>
<h3 className="font-semibold">{item.title}</h3>
<p>
{item.kind === 'service'
? 'Prestação de serviços'
: 'Recrutamento'}{' '}
· Prazo: {item.closes_on}
</p>
<details>
<summary className="cursor-pointer underline">
Consultar descrição publicada
</summary>
<p className="whitespace-pre-wrap">
{item.description}
</p>
</details>
<a
className="inline-block underline"
href="#candidate-login"
>
Entrar para preparar a candidatura
</a>
</article>
))}
<nav
aria-label="Páginas de oportunidades"
className="flex gap-4"
>
{opportunities.prev_page_url && (
<Link
href={opportunities.prev_page_url}
preserveScroll
>
Anterior
</Link>
)}
{opportunities.next_page_url && (
<Link
href={opportunities.next_page_url}
preserveScroll
>
Seguinte
</Link>
)}
</nav>
</section>
<section className="space-y-3" id="candidate-login">
<h2 className="text-xl font-semibold">Entrar</h2>
<form
className="space-y-3"
+17
View File
@@ -17,6 +17,7 @@ export default function Profile({
emailDeliveryAvailable = false,
accessStatus,
opportunities = [],
opportunityPagination,
submissions = [],
}: {
companyId: number;
@@ -26,6 +27,7 @@ export default function Profile({
emailDeliveryAvailable?: boolean;
accessStatus?: string | null;
opportunities?: { id: number; title: string; closes_on: string }[];
opportunityPagination?: { prev: string | null; next: string | null };
submissions?: { id: number; reference: string; created_at: string }[];
}) {
const form = useForm({
@@ -52,6 +54,21 @@ export default function Profile({
</Link>
</p>
))}
<nav
aria-label="Páginas de oportunidades"
className="flex gap-4"
>
{opportunityPagination?.prev && (
<Link href={opportunityPagination.prev} preserveScroll>
Anterior
</Link>
)}
{opportunityPagination?.next && (
<Link href={opportunityPagination.next} preserveScroll>
Seguinte
</Link>
)}
</nav>
<h2 className="text-xl font-semibold">
As minhas candidaturas
</h2>
@@ -12,6 +12,7 @@ use App\Services\RequisitionWorkflow;
use App\Services\SubmissionForms;
use Illuminate\Database\QueryException;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
@@ -44,6 +45,43 @@ beforeEach(function () {
'answers' => ['full_name' => 'Pessoa fictícia', 'availability_location' => $this->profile['availability']]];
});
test('guests see only explicitly published public opportunity fields while submission remains authenticated', function () {
Auth::guard('candidate')->logout();
Auth::guard('web')->logout();
$this->get($this->base)->assertInertia(fn (Assert $p) => $p->has('opportunities.data', 1)
->where('opportunities.data.0', ['id' => $this->process->id, 'title' => 'Oportunidade fictícia', 'kind' => 'service', 'closes_on' => $this->process->closes_on, 'description' => $this->publication['description']])
->where('auth.user', null)->where('organizationAccess', null));
$this->get($this->base.'/oportunidades/'.$this->process->id)->assertUnauthorized();
$this->postJson($this->base.'/oportunidades/'.$this->process->id, $this->body)->assertUnauthorized();
expect(DB::table('candidate_submissions')->count())->toBe(0);
$this->actingAs($this->admin);
app(CandidateSubmissions::class)->publish($this->admin, $this->process->id, [...$this->publication, 'version' => 1, 'active' => false]);
$this->get($this->base)->assertInertia(fn (Assert $p) => $p->has('opportunities.data', 0));
app(CandidateSubmissions::class)->publish($this->admin, $this->process->id, [...$this->publication, 'version' => 2]);
$this->travel(8)->days();
$this->get($this->base)->assertInertia(fn (Assert $p) => $p->has('opportunities.data', 0));
});
test('public opportunity listing is paginated within the company and remains visible to existing users when registration closes', function () {
$process = (array) DB::table('people_processes')->where('id', $this->process->id)->first();
$publication = (array) DB::table('candidate_opportunity_publications')->where('people_process_id', $this->process->id)->first();
unset($process['id'], $publication['id']);
for ($index = 0; $index < 12; $index++) {
$id = DB::table('people_processes')->insertGetId([...$process, 'operation_key' => (string) Str::uuid()]);
DB::table('candidate_opportunity_publications')->insert([...$publication, 'people_process_id' => $id]);
}
app(CandidateProfiles::class)->configure($this->admin, ['version' => 1, 'registration_open' => false, 'notice' => 'Aviso fictício de registos temporariamente encerrados.', 'reason' => 'Encerrar novos registos no ensaio.']);
$this->get($this->base)->assertInertia(fn (Assert $p) => $p->has('opportunities.data', 12)->where('opportunities.total', 13)->where('opportunities.next_page_url', $this->base.'?opportunities_page=2')->where('portal.registration_open', false));
$this->get($this->base.'?opportunities_page=2')->assertInertia(fn (Assert $p) => $p->has('opportunities.data', 1));
$this->get($this->base.'/perfil?opportunities_page=2')->assertInertia(fn (Assert $p) => $p->has('opportunities', 1)->where('opportunityPagination.prev', $this->base.'/perfil?opportunities_page=1')->where('opportunityPagination.next', null));
$other = Tenant::query()->create(['name' => 'Empresa sem oportunidades']);
$otherAdmin = User::factory()->create(['tenant_id' => $other->id, 'purchasing_role' => PurchasingRole::Admin]);
$this->actingAs($otherAdmin);
app(CandidateProfiles::class)->configure($otherAdmin, ['version' => 0, 'registration_open' => true, 'notice' => 'Aviso fictício da outra empresa sem oportunidades.', 'reason' => 'Configurar portal separado no ensaio.']);
$this->get('/candidaturas/'.$other->id)->assertInertia(fn (Assert $p) => $p->has('opportunities.data', 0));
$this->getJson($this->base.'?opportunities_page=invalid')->assertUnprocessable();
});
test('electronic submission returns a scoped receipt and preserves the actual profile and answers', function () {
$this->get($this->base.'/oportunidades/'.$this->process->id)->assertInertia(fn (Assert $p) => $p->where('opportunity.description', 'Descrição pública fictícia da oportunidade.')->missing('opportunity.purpose')->missing('opportunity.selection'));
$url = $this->base.'/oportunidades/'.$this->process->id;
+38
View File
@@ -62,6 +62,44 @@ test.each([true, false])(
expect(html).not.toContain('/dashboard');
},
);
test('public entry shows published opportunities and pagination while keeping application behind candidate login', () => {
const initialPage = {
...page,
props: {
...page.props,
company: { id: 1, name: 'Empresa fictícia' },
portal: {
version: 1,
registration_open: false,
notice: 'Aviso fictício.',
},
opportunities: {
data: [
{
id: 7,
title: 'Serviço publicado',
kind: 'service',
closes_on: '2026-11-10',
description: 'Descrição pública do serviço.',
},
],
prev_page_url: null,
next_page_url: '/candidaturas/1?opportunities_page=2',
},
},
};
const html = renderToString(
<App initialPage={initialPage} initialComponent={Entry} />,
);
expect(html).toContain('Serviço publicado');
expect(html).toContain('Descrição pública do serviço.');
expect(html).toContain('Prestação de serviços');
expect(html).toContain('href="#candidate-login"');
expect(html).toContain('/candidaturas/1?opportunities_page=2');
expect(html).not.toContain('/oportunidades/7');
expect(html).not.toContain('anexos ainda está em desenvolvimento');
});
test('candidate profile renders geography and retained version without internal evaluation controls', () => {
const initialPage = {
...page,