feat(form-builder): add PurposeSubjectResolver per purpose (WS-6)
Parallel interface to PurposeGuardProvider for runtime subject resolution. Seven concrete resolvers, one per v1.0 purpose. Wired through purposes.php via subject_resolver_class key. EventRegistration uses PersonProvisioner (may create). Other purposes resolve from existing context (portal token, production request, auth). IncidentReport is the only purpose allowed to return null (anonymous- allowed configurations); the others return concrete model types (narrowed via PHP covariance) for caller convenience. Refs: RFC-WS-6.md §3 (Q9) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions\FormBuilder;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* RFC-WS-6 §3 (Q9) — failure during PurposeSubjectResolver operation.
|
||||
*
|
||||
* Reason codes (informal contract):
|
||||
* - 'no_portal_token' artist_advance has no portal context
|
||||
* - 'no_production_request' supplier_intake has no production_request link
|
||||
* - 'no_auth' auth-based purpose has no authenticated user
|
||||
* - 'no_person_for_user' user has no linked Person
|
||||
* - 'subject_not_found' subject_type/subject_id set but record gone
|
||||
*/
|
||||
final class PurposeSubjectResolutionException extends RuntimeException
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $purposeSlug,
|
||||
public readonly string $reasonCode,
|
||||
public readonly string $submissionId,
|
||||
?string $message = null,
|
||||
) {
|
||||
parent::__construct($message ?? "Subject resolution failed for {$purposeSlug}: {$reasonCode} (submission {$submissionId})");
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,12 @@ final class PurposeRegistry
|
||||
/** @var array<string, PurposeGuardProvider> */
|
||||
private array $guardProviderCache = [];
|
||||
|
||||
/** @var array<string, class-string<PurposeSubjectResolver>>|null */
|
||||
private ?array $resolverClassCache = null;
|
||||
|
||||
/** @var array<string, PurposeSubjectResolver> */
|
||||
private array $resolverInstanceCache = [];
|
||||
|
||||
public function __construct(private readonly ConfigRepository $config) {}
|
||||
|
||||
/** @return array<string, PurposeDefinition> keyed by slug */
|
||||
@@ -33,6 +39,7 @@ final class PurposeRegistry
|
||||
|
||||
$definitions = [];
|
||||
$guardClasses = [];
|
||||
$resolverClasses = [];
|
||||
foreach ($raw as $slug => $attrs) {
|
||||
$mode = $attrs['default_submission_mode'] ?? null;
|
||||
if (! $mode instanceof FormSubmissionMode) {
|
||||
@@ -48,6 +55,13 @@ final class PurposeRegistry
|
||||
);
|
||||
}
|
||||
|
||||
$resolverClass = $attrs['subject_resolver_class'] ?? null;
|
||||
if (! is_string($resolverClass) || ! is_subclass_of($resolverClass, PurposeSubjectResolver::class)) {
|
||||
throw new \InvalidArgumentException(
|
||||
"Purpose '{$slug}' has invalid subject_resolver_class; expected class-string implementing PurposeSubjectResolver."
|
||||
);
|
||||
}
|
||||
|
||||
$definitions[(string) $slug] = new PurposeDefinition(
|
||||
slug: (string) $slug,
|
||||
label: (string) ($attrs['label'] ?? ''),
|
||||
@@ -57,9 +71,11 @@ final class PurposeRegistry
|
||||
requiredBindings: array_values((array) ($attrs['required_bindings'] ?? [])),
|
||||
);
|
||||
$guardClasses[(string) $slug] = $guardsClass;
|
||||
$resolverClasses[(string) $slug] = $resolverClass;
|
||||
}
|
||||
|
||||
$this->guardClassCache = $guardClasses;
|
||||
$this->resolverClassCache = $resolverClasses;
|
||||
|
||||
return $this->cache = $definitions;
|
||||
}
|
||||
@@ -84,6 +100,26 @@ final class PurposeRegistry
|
||||
return $this->guardProviderCache[$slug] = $instance;
|
||||
}
|
||||
|
||||
public function subjectResolverFor(string $slug): PurposeSubjectResolver
|
||||
{
|
||||
if (! $this->has($slug)) {
|
||||
throw Exceptions\PurposeNotFoundException::forSlug($slug);
|
||||
}
|
||||
|
||||
if (isset($this->resolverInstanceCache[$slug])) {
|
||||
return $this->resolverInstanceCache[$slug];
|
||||
}
|
||||
|
||||
/** @var array<string, class-string<PurposeSubjectResolver>> $classes */
|
||||
$classes = $this->resolverClassCache ?? [];
|
||||
$class = $classes[$slug];
|
||||
|
||||
/** @var PurposeSubjectResolver $instance */
|
||||
$instance = resolve($class);
|
||||
|
||||
return $this->resolverInstanceCache[$slug] = $instance;
|
||||
}
|
||||
|
||||
public function get(string $slug): PurposeDefinition
|
||||
{
|
||||
$all = $this->all();
|
||||
|
||||
27
api/app/FormBuilder/Purposes/PurposeSubjectResolver.php
Normal file
27
api/app/FormBuilder/Purposes/PurposeSubjectResolver.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\FormBuilder\Purposes;
|
||||
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* RFC-WS-6 §3 (Q9) — parallel interface to PurposeGuardProvider for
|
||||
* runtime subject resolution. PurposeDefinition stays a frozen value
|
||||
* object; this contract lives separately.
|
||||
*
|
||||
* Returns null only for purposes that allow anonymous submissions
|
||||
* (currently incident_report). All other purposes throw on failure.
|
||||
*
|
||||
* Called from inside FormBindingApplicator within a DB::transaction so
|
||||
* any lockForUpdate semantics persist.
|
||||
*/
|
||||
interface PurposeSubjectResolver
|
||||
{
|
||||
/**
|
||||
* @throws \App\Exceptions\FormBuilder\PurposeSubjectResolutionException
|
||||
*/
|
||||
public function resolveOrProvision(FormSubmission $submission): ?Model;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\FormBuilder\Purposes\Resolvers;
|
||||
|
||||
use App\Exceptions\FormBuilder\PurposeSubjectResolutionException;
|
||||
use App\FormBuilder\Purposes\PurposeSubjectResolver;
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* RFC §3 Q9 — artist_advance: subject is the Artist resolved via the
|
||||
* portal token that opened the submission. Throws if no token.
|
||||
*
|
||||
* The Artist model is not yet implemented (BACKLOG: ARCH-09); this
|
||||
* resolver short-circuits when the submission already carries
|
||||
* subject_type='artist' (set by the portal route) — the typical path.
|
||||
* If subject is absent, throw — the route should never have allowed
|
||||
* the submission through without a portal token.
|
||||
*/
|
||||
final readonly class ArtistAdvanceSubjectResolver implements PurposeSubjectResolver
|
||||
{
|
||||
public function resolveOrProvision(FormSubmission $submission): Model
|
||||
{
|
||||
if ($submission->subject_type === 'artist' && $submission->subject_id !== null) {
|
||||
$subject = $submission->subject;
|
||||
if ($subject !== null) {
|
||||
return $subject;
|
||||
}
|
||||
throw new PurposeSubjectResolutionException(
|
||||
'artist_advance',
|
||||
'subject_not_found',
|
||||
(string) $submission->id,
|
||||
"submission claims artist subject {$submission->subject_id} but record is gone",
|
||||
);
|
||||
}
|
||||
|
||||
throw new PurposeSubjectResolutionException(
|
||||
'artist_advance',
|
||||
'no_portal_token',
|
||||
(string) $submission->id,
|
||||
'artist_advance submission has no resolved Artist; portal token missing',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\FormBuilder\Purposes\Resolvers;
|
||||
|
||||
use App\FormBuilder\Bindings\PersonProvisioner;
|
||||
use App\FormBuilder\Purposes\PurposeSubjectResolver;
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use App\Models\Person;
|
||||
|
||||
/**
|
||||
* RFC §3 Q9 — event_registration: provisions a Person via
|
||||
* PersonProvisioner (firstOrCreate semantics). Subject may be created.
|
||||
*/
|
||||
final readonly class EventRegistrationSubjectResolver implements PurposeSubjectResolver
|
||||
{
|
||||
public function __construct(private PersonProvisioner $provisioner) {}
|
||||
|
||||
public function resolveOrProvision(FormSubmission $submission): Person
|
||||
{
|
||||
return $this->provisioner->provisionFromSubmission($submission);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\FormBuilder\Purposes\Resolvers;
|
||||
|
||||
use App\FormBuilder\Purposes\PurposeSubjectResolver;
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use App\Models\Person;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* RFC §3 Q9 — incident_report: subject is Person via auth when present;
|
||||
* null when anonymous (the only purpose that may legitimately resolve
|
||||
* to no subject).
|
||||
*/
|
||||
final readonly class IncidentReportSubjectResolver implements PurposeSubjectResolver
|
||||
{
|
||||
public function resolveOrProvision(FormSubmission $submission): ?Model
|
||||
{
|
||||
if ($submission->subject_type === 'person' && $submission->subject_id !== null) {
|
||||
$subject = $submission->subject;
|
||||
if ($subject instanceof Person) {
|
||||
return $subject;
|
||||
}
|
||||
}
|
||||
|
||||
if ($submission->submitted_by_user_id === null) {
|
||||
// Anonymous-allowed: caller (FormBindingApplicator) handles
|
||||
// the null subject path explicitly.
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = User::query()->find($submission->submitted_by_user_id);
|
||||
if (! $user instanceof User) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Person::query()
|
||||
->withoutGlobalScopes()
|
||||
->where('user_id', $user->id)
|
||||
->where('event_id', $submission->event_id)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\FormBuilder\Purposes\Resolvers;
|
||||
|
||||
use App\Exceptions\FormBuilder\PurposeSubjectResolutionException;
|
||||
use App\FormBuilder\Purposes\PurposeSubjectResolver;
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use App\Models\Person;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* RFC §3 Q9 — post_event_evaluation: subject is a Person resolved via
|
||||
* the authenticated User → linked Person. Throws if no auth or no
|
||||
* Person link.
|
||||
*/
|
||||
final readonly class PostEventEvaluationSubjectResolver implements PurposeSubjectResolver
|
||||
{
|
||||
public function resolveOrProvision(FormSubmission $submission): Model
|
||||
{
|
||||
if ($submission->subject_type === 'person' && $submission->subject_id !== null) {
|
||||
$subject = $submission->subject;
|
||||
if ($subject instanceof Person) {
|
||||
return $subject;
|
||||
}
|
||||
}
|
||||
|
||||
$user = $this->resolveUser($submission);
|
||||
$person = Person::query()->withoutGlobalScopes()->where('user_id', $user->id)
|
||||
->where('event_id', $submission->event_id)
|
||||
->first();
|
||||
|
||||
if ($person === null) {
|
||||
throw new PurposeSubjectResolutionException(
|
||||
'post_event_evaluation',
|
||||
'no_person_for_user',
|
||||
(string) $submission->id,
|
||||
"user {$user->id} has no Person linked for event {$submission->event_id}",
|
||||
);
|
||||
}
|
||||
|
||||
return $person;
|
||||
}
|
||||
|
||||
private function resolveUser(FormSubmission $submission): User
|
||||
{
|
||||
if ($submission->submitted_by_user_id !== null) {
|
||||
$user = User::query()->find($submission->submitted_by_user_id);
|
||||
if ($user instanceof User) {
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
throw new PurposeSubjectResolutionException(
|
||||
'post_event_evaluation',
|
||||
'no_auth',
|
||||
(string) $submission->id,
|
||||
'post_event_evaluation submission has no authenticated User',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\FormBuilder\Purposes\Resolvers;
|
||||
|
||||
use App\Exceptions\FormBuilder\PurposeSubjectResolutionException;
|
||||
use App\FormBuilder\Purposes\PurposeSubjectResolver;
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* RFC §3 Q9 — signature_contract: subject is the authenticated User.
|
||||
*/
|
||||
final readonly class SignatureContractSubjectResolver implements PurposeSubjectResolver
|
||||
{
|
||||
public function resolveOrProvision(FormSubmission $submission): User
|
||||
{
|
||||
if ($submission->subject_type === 'user' && $submission->subject_id !== null) {
|
||||
$subject = $submission->subject;
|
||||
if ($subject instanceof User) {
|
||||
return $subject;
|
||||
}
|
||||
}
|
||||
|
||||
if ($submission->submitted_by_user_id === null) {
|
||||
throw new PurposeSubjectResolutionException(
|
||||
'signature_contract',
|
||||
'no_auth',
|
||||
(string) $submission->id,
|
||||
'signature_contract submission has no authenticated User',
|
||||
);
|
||||
}
|
||||
|
||||
$user = User::query()->find($submission->submitted_by_user_id);
|
||||
if (! $user instanceof User) {
|
||||
throw new PurposeSubjectResolutionException(
|
||||
'signature_contract',
|
||||
'no_auth',
|
||||
(string) $submission->id,
|
||||
"submitted_by_user_id {$submission->submitted_by_user_id} not found",
|
||||
);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\FormBuilder\Purposes\Resolvers;
|
||||
|
||||
use App\Exceptions\FormBuilder\PurposeSubjectResolutionException;
|
||||
use App\FormBuilder\Purposes\PurposeSubjectResolver;
|
||||
use App\Models\Company;
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* RFC §3 Q9 — supplier_intake: subject is the Company associated with
|
||||
* the production_request that created this submission. Throws if no
|
||||
* Company subject is set (route should validate upstream).
|
||||
*/
|
||||
final readonly class SupplierIntakeSubjectResolver implements PurposeSubjectResolver
|
||||
{
|
||||
public function resolveOrProvision(FormSubmission $submission): Company
|
||||
{
|
||||
if ($submission->subject_type === 'company' && $submission->subject_id !== null) {
|
||||
$subject = $submission->subject;
|
||||
if ($subject instanceof Company) {
|
||||
return $subject;
|
||||
}
|
||||
throw new PurposeSubjectResolutionException(
|
||||
'supplier_intake',
|
||||
'subject_not_found',
|
||||
(string) $submission->id,
|
||||
"submission claims company subject {$submission->subject_id} but record is gone",
|
||||
);
|
||||
}
|
||||
|
||||
throw new PurposeSubjectResolutionException(
|
||||
'supplier_intake',
|
||||
'no_production_request',
|
||||
(string) $submission->id,
|
||||
'supplier_intake submission has no Company subject — production_request link missing',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\FormBuilder\Purposes\Resolvers;
|
||||
|
||||
use App\Exceptions\FormBuilder\PurposeSubjectResolutionException;
|
||||
use App\FormBuilder\Purposes\PurposeSubjectResolver;
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* RFC §3 Q9 — user_profile: subject is the authenticated User.
|
||||
*/
|
||||
final readonly class UserProfileSubjectResolver implements PurposeSubjectResolver
|
||||
{
|
||||
public function resolveOrProvision(FormSubmission $submission): User
|
||||
{
|
||||
if ($submission->subject_type === 'user' && $submission->subject_id !== null) {
|
||||
$subject = $submission->subject;
|
||||
if ($subject instanceof User) {
|
||||
return $subject;
|
||||
}
|
||||
}
|
||||
|
||||
if ($submission->submitted_by_user_id === null) {
|
||||
throw new PurposeSubjectResolutionException(
|
||||
'user_profile',
|
||||
'no_auth',
|
||||
(string) $submission->id,
|
||||
'user_profile submission has no authenticated User',
|
||||
);
|
||||
}
|
||||
|
||||
$user = User::query()->find($submission->submitted_by_user_id);
|
||||
if (! $user instanceof User) {
|
||||
throw new PurposeSubjectResolutionException(
|
||||
'user_profile',
|
||||
'no_auth',
|
||||
(string) $submission->id,
|
||||
"submitted_by_user_id {$submission->submitted_by_user_id} not found",
|
||||
);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ return [
|
||||
'allows_public_access' => true,
|
||||
'required_bindings' => ['person.email', 'person.first_name', 'person.last_name'],
|
||||
'guards_class' => \App\FormBuilder\Purposes\Guards\EventRegistrationGuards::class,
|
||||
'subject_resolver_class' => \App\FormBuilder\Purposes\Resolvers\EventRegistrationSubjectResolver::class,
|
||||
],
|
||||
|
||||
'artist_advance' => [
|
||||
@@ -42,6 +43,7 @@ return [
|
||||
'allows_public_access' => false,
|
||||
'required_bindings' => [],
|
||||
'guards_class' => \App\FormBuilder\Purposes\Guards\ArtistAdvanceGuards::class,
|
||||
'subject_resolver_class' => \App\FormBuilder\Purposes\Resolvers\ArtistAdvanceSubjectResolver::class,
|
||||
],
|
||||
|
||||
'supplier_intake' => [
|
||||
@@ -51,6 +53,7 @@ return [
|
||||
'allows_public_access' => false,
|
||||
'required_bindings' => ['company.name'],
|
||||
'guards_class' => \App\FormBuilder\Purposes\Guards\SupplierIntakeGuards::class,
|
||||
'subject_resolver_class' => \App\FormBuilder\Purposes\Resolvers\SupplierIntakeSubjectResolver::class,
|
||||
],
|
||||
|
||||
'post_event_evaluation' => [
|
||||
@@ -60,6 +63,7 @@ return [
|
||||
'allows_public_access' => false,
|
||||
'required_bindings' => [],
|
||||
'guards_class' => \App\FormBuilder\Purposes\Guards\PostEventEvaluationGuards::class,
|
||||
'subject_resolver_class' => \App\FormBuilder\Purposes\Resolvers\PostEventEvaluationSubjectResolver::class,
|
||||
],
|
||||
|
||||
'incident_report' => [
|
||||
@@ -69,6 +73,7 @@ return [
|
||||
'allows_public_access' => false,
|
||||
'required_bindings' => [],
|
||||
'guards_class' => \App\FormBuilder\Purposes\Guards\IncidentReportGuards::class,
|
||||
'subject_resolver_class' => \App\FormBuilder\Purposes\Resolvers\IncidentReportSubjectResolver::class,
|
||||
],
|
||||
|
||||
'signature_contract' => [
|
||||
@@ -78,6 +83,7 @@ return [
|
||||
'allows_public_access' => false,
|
||||
'required_bindings' => [],
|
||||
'guards_class' => \App\FormBuilder\Purposes\Guards\SignatureContractGuards::class,
|
||||
'subject_resolver_class' => \App\FormBuilder\Purposes\Resolvers\SignatureContractSubjectResolver::class,
|
||||
],
|
||||
|
||||
'user_profile' => [
|
||||
@@ -87,6 +93,7 @@ return [
|
||||
'allows_public_access' => false,
|
||||
'required_bindings' => [],
|
||||
'guards_class' => \App\FormBuilder\Purposes\Guards\UserProfileGuards::class,
|
||||
'subject_resolver_class' => \App\FormBuilder\Purposes\Resolvers\UserProfileSubjectResolver::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\FormBuilder\Purposes;
|
||||
|
||||
use App\FormBuilder\Purposes\PurposeRegistry;
|
||||
use App\FormBuilder\Purposes\PurposeSubjectResolver;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class AllPurposesSubjectResolverWiringTest extends TestCase
|
||||
{
|
||||
public function test_every_purpose_has_a_subject_resolver_class(): void
|
||||
{
|
||||
/** @var array<string, mixed> $config */
|
||||
$config = config('form_builder.purposes');
|
||||
$this->assertNotEmpty($config);
|
||||
|
||||
foreach ($config as $slug => $attrs) {
|
||||
$this->assertArrayHasKey(
|
||||
'subject_resolver_class',
|
||||
$attrs,
|
||||
"Purpose '{$slug}' is missing the subject_resolver_class config key.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_registry_resolves_a_subject_resolver_for_every_purpose(): void
|
||||
{
|
||||
$registry = $this->app->make(PurposeRegistry::class);
|
||||
|
||||
foreach (array_keys($registry->all()) as $slug) {
|
||||
$resolver = $registry->subjectResolverFor($slug);
|
||||
$this->assertInstanceOf(PurposeSubjectResolver::class, $resolver);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit\FormBuilder\Purposes\Resolvers;
|
||||
|
||||
use App\Exceptions\FormBuilder\PurposeSubjectResolutionException;
|
||||
use App\FormBuilder\Purposes\Resolvers\ArtistAdvanceSubjectResolver;
|
||||
use App\FormBuilder\Purposes\Resolvers\IncidentReportSubjectResolver;
|
||||
use App\FormBuilder\Purposes\Resolvers\PostEventEvaluationSubjectResolver;
|
||||
use App\FormBuilder\Purposes\Resolvers\SignatureContractSubjectResolver;
|
||||
use App\FormBuilder\Purposes\Resolvers\SupplierIntakeSubjectResolver;
|
||||
use App\FormBuilder\Purposes\Resolvers\UserProfileSubjectResolver;
|
||||
use App\Models\Company;
|
||||
use App\Models\CrowdType;
|
||||
use App\Models\Event;
|
||||
use App\Models\FormBuilder\FormSchema;
|
||||
use App\Models\FormBuilder\FormSubmission;
|
||||
use App\Models\Organisation;
|
||||
use App\Models\Person;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class PurposeSubjectResolversTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_artist_advance_throws_without_subject(): void
|
||||
{
|
||||
$submission = FormSubmission::factory()->create();
|
||||
|
||||
try {
|
||||
$this->app->make(ArtistAdvanceSubjectResolver::class)->resolveOrProvision($submission);
|
||||
$this->fail('Expected PurposeSubjectResolutionException');
|
||||
} catch (PurposeSubjectResolutionException $e) {
|
||||
$this->assertSame('artist_advance', $e->purposeSlug);
|
||||
$this->assertSame('no_portal_token', $e->reasonCode);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_supplier_intake_returns_company_subject(): void
|
||||
{
|
||||
$organisation = Organisation::factory()->create();
|
||||
$company = Company::factory()->create(['organisation_id' => $organisation->id]);
|
||||
$submission = FormSubmission::factory()->forOrganisation($organisation)->create([
|
||||
'subject_type' => 'company',
|
||||
'subject_id' => $company->id,
|
||||
]);
|
||||
|
||||
$resolved = $this->app->make(SupplierIntakeSubjectResolver::class)
|
||||
->resolveOrProvision($submission->fresh());
|
||||
|
||||
$this->assertInstanceOf(Company::class, $resolved);
|
||||
$this->assertSame($company->id, $resolved->id);
|
||||
}
|
||||
|
||||
public function test_supplier_intake_throws_without_company(): void
|
||||
{
|
||||
$submission = FormSubmission::factory()->create();
|
||||
|
||||
try {
|
||||
$this->app->make(SupplierIntakeSubjectResolver::class)->resolveOrProvision($submission);
|
||||
$this->fail('Expected PurposeSubjectResolutionException');
|
||||
} catch (PurposeSubjectResolutionException $e) {
|
||||
$this->assertSame('no_production_request', $e->reasonCode);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_post_event_evaluation_resolves_person_via_user(): void
|
||||
{
|
||||
$event = Event::factory()->create();
|
||||
$crowdType = CrowdType::factory()->create(['organisation_id' => $event->organisation_id]);
|
||||
$user = User::factory()->create();
|
||||
$person = Person::factory()->create([
|
||||
'event_id' => $event->id,
|
||||
'crowd_type_id' => $crowdType->id,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
||||
$submission = FormSubmission::factory()->forEvent($event)->create([
|
||||
'submitted_by_user_id' => $user->id,
|
||||
]);
|
||||
|
||||
$resolved = $this->app->make(PostEventEvaluationSubjectResolver::class)
|
||||
->resolveOrProvision($submission->fresh());
|
||||
|
||||
$this->assertInstanceOf(Person::class, $resolved);
|
||||
$this->assertSame($person->id, $resolved->id);
|
||||
}
|
||||
|
||||
public function test_post_event_evaluation_throws_when_no_auth(): void
|
||||
{
|
||||
$submission = FormSubmission::factory()->create(['submitted_by_user_id' => null]);
|
||||
|
||||
try {
|
||||
$this->app->make(PostEventEvaluationSubjectResolver::class)->resolveOrProvision($submission);
|
||||
$this->fail('Expected PurposeSubjectResolutionException');
|
||||
} catch (PurposeSubjectResolutionException $e) {
|
||||
$this->assertSame('no_auth', $e->reasonCode);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_incident_report_returns_null_when_anonymous(): void
|
||||
{
|
||||
$submission = FormSubmission::factory()->create(['submitted_by_user_id' => null]);
|
||||
|
||||
$resolved = $this->app->make(IncidentReportSubjectResolver::class)
|
||||
->resolveOrProvision($submission);
|
||||
|
||||
$this->assertNull($resolved);
|
||||
}
|
||||
|
||||
public function test_signature_contract_returns_user_subject(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$submission = FormSubmission::factory()->create([
|
||||
'submitted_by_user_id' => $user->id,
|
||||
]);
|
||||
|
||||
$resolved = $this->app->make(SignatureContractSubjectResolver::class)
|
||||
->resolveOrProvision($submission);
|
||||
|
||||
$this->assertInstanceOf(User::class, $resolved);
|
||||
$this->assertSame($user->id, $resolved->id);
|
||||
}
|
||||
|
||||
public function test_signature_contract_throws_without_auth(): void
|
||||
{
|
||||
$submission = FormSubmission::factory()->create(['submitted_by_user_id' => null]);
|
||||
|
||||
try {
|
||||
$this->app->make(SignatureContractSubjectResolver::class)->resolveOrProvision($submission);
|
||||
$this->fail('Expected PurposeSubjectResolutionException');
|
||||
} catch (PurposeSubjectResolutionException $e) {
|
||||
$this->assertSame('no_auth', $e->reasonCode);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_user_profile_returns_user_subject(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$submission = FormSubmission::factory()->create([
|
||||
'submitted_by_user_id' => $user->id,
|
||||
]);
|
||||
|
||||
$resolved = $this->app->make(UserProfileSubjectResolver::class)
|
||||
->resolveOrProvision($submission);
|
||||
|
||||
$this->assertInstanceOf(User::class, $resolved);
|
||||
$this->assertSame($user->id, $resolved->id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user