From 99879dea62c1e32d6926ee5e665de837910b8b34 Mon Sep 17 00:00:00 2001 From: Tonko Mulder Date: Thu, 9 Jul 2026 15:12:20 +0200 Subject: [PATCH 1/4] add enums ApplicationStatus and InteractionType --- app/Enums/ApplicationStatus.php | 15 +++++++++++++++ app/Enums/InteractionType.php | 14 ++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 app/Enums/ApplicationStatus.php create mode 100644 app/Enums/InteractionType.php diff --git a/app/Enums/ApplicationStatus.php b/app/Enums/ApplicationStatus.php new file mode 100644 index 0000000..2561d14 --- /dev/null +++ b/app/Enums/ApplicationStatus.php @@ -0,0 +1,15 @@ + Date: Thu, 9 Jul 2026 16:17:40 +0200 Subject: [PATCH 2/4] add models/migrations according to specs --- app/Models/Application.php | 55 +++++++++++++++++++ app/Models/Company.php | 55 +++++++++++++++++++ app/Models/Contact.php | 44 +++++++++++++++ app/Models/Interaction.php | 52 ++++++++++++++++++ ...26_07_09_131447_create_companies_table.php | 38 +++++++++++++ ...07_09_131728_create_applications_table.php | 36 ++++++++++++ ...026_07_09_131821_create_contacts_table.php | 32 +++++++++++ ...07_09_131838_create_interactions_table.php | 34 ++++++++++++ 8 files changed, 346 insertions(+) create mode 100644 app/Models/Application.php create mode 100644 app/Models/Company.php create mode 100644 app/Models/Contact.php create mode 100644 app/Models/Interaction.php create mode 100644 database/migrations/2026_07_09_131447_create_companies_table.php create mode 100644 database/migrations/2026_07_09_131728_create_applications_table.php create mode 100644 database/migrations/2026_07_09_131821_create_contacts_table.php create mode 100644 database/migrations/2026_07_09_131838_create_interactions_table.php diff --git a/app/Models/Application.php b/app/Models/Application.php new file mode 100644 index 0000000..91f134b --- /dev/null +++ b/app/Models/Application.php @@ -0,0 +1,55 @@ + $interactions + */ +final class Application extends Model +{ + protected $fillable = [ + 'company_id', + 'role_title', + 'source', + 'status', + 'applied_at', + 'notes', + ]; + + /** @phpstan-return BelongsTo */ + public function company(): BelongsTo + { + return $this->belongsTo(Company::class); + } + + /** @phpstan-return HasMany */ + public function interactions(): HasMany + { + return $this->hasMany(Interaction::class); + } + + protected function casts(): array + { + return [ + 'status' => ApplicationStatus::class, + 'applied_at' => 'immutable_datetime', + ]; + } +} diff --git a/app/Models/Company.php b/app/Models/Company.php new file mode 100644 index 0000000..4181659 --- /dev/null +++ b/app/Models/Company.php @@ -0,0 +1,55 @@ + $applications + * @property-read \Illuminate\Database\Eloquent\Collection $contacts + */ +final class Company extends Model +{ + protected $fillable = [ + 'name', + 'kvk_number', + 'website', + 'city', + 'sbi_code', + 'sbi_description', + 'enriched_at', + ]; + + /** @phpstan-return HasMany */ + public function applications(): HasMany + { + return $this->hasMany(Application::class); + } + + /** @phpstan-return HasMany */ + public function contacts(): HasMany + { + return $this->hasMany(Contact::class); + } + + protected function casts(): array + { + return [ + 'enriched_at' => 'immutable_datetime', + ]; + } +} diff --git a/app/Models/Contact.php b/app/Models/Contact.php new file mode 100644 index 0000000..d40690f --- /dev/null +++ b/app/Models/Contact.php @@ -0,0 +1,44 @@ + $interactions + */ +final class Contact extends Model +{ + protected $fillable = [ + 'company_id', + 'name', + 'role', + 'email', + 'phone', + ]; + + /** @phpstan-return BelongsTo */ + public function company(): BelongsTo + { + return $this->belongsTo(Company::class); + } + + /** @phpstan-return HasMany */ + public function interactions(): HasMany + { + return $this->hasMany(Interaction::class); + } +} diff --git a/app/Models/Interaction.php b/app/Models/Interaction.php new file mode 100644 index 0000000..102452c --- /dev/null +++ b/app/Models/Interaction.php @@ -0,0 +1,52 @@ + */ + public function application(): BelongsTo + { + return $this->belongsTo(Application::class); + } + + /** @phpstan-return BelongsTo */ + public function contact(): BelongsTo + { + return $this->belongsTo(Contact::class); + } + + protected function casts(): array + { + return [ + 'type' => InteractionType::class, + 'occurred_at' => 'immutable_datetime', + ]; + } +} diff --git a/database/migrations/2026_07_09_131447_create_companies_table.php b/database/migrations/2026_07_09_131447_create_companies_table.php new file mode 100644 index 0000000..475d995 --- /dev/null +++ b/database/migrations/2026_07_09_131447_create_companies_table.php @@ -0,0 +1,38 @@ +id(); + $table->string('name'); + $table->string('kvk_number')->nullable(); + $table->string('website')->nullable(); + $table->string('city')->nullable(); + $table->string('sbi_code') + ->nullable() + ->comment('from KVK'); + $table->string('sbi_description') + ->nullable() + ->comment('from KVK'); + $table->timestamp('enriched_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('companies'); + } +}; diff --git a/database/migrations/2026_07_09_131728_create_applications_table.php b/database/migrations/2026_07_09_131728_create_applications_table.php new file mode 100644 index 0000000..24df413 --- /dev/null +++ b/database/migrations/2026_07_09_131728_create_applications_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignIdFor(\App\Models\Company::class); + $table->string('role_title') + ->comment('e.g. "Medior Laravel Developer"'); + $table->string('source') + ->nullable() + ->comment('recruiter / direct'); + $table->string('status')->index(); + $table->timestamp('applied_at')->nullable(); + $table->text('notes')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('applications'); + } +}; diff --git a/database/migrations/2026_07_09_131821_create_contacts_table.php b/database/migrations/2026_07_09_131821_create_contacts_table.php new file mode 100644 index 0000000..f8d8587 --- /dev/null +++ b/database/migrations/2026_07_09_131821_create_contacts_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignIdFor(\App\Models\Company::class); + $table->string('name'); + $table->string('role')->nullable(); + $table->string('email')->nullable(); + $table->string('phone')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('contacts'); + } +}; diff --git a/database/migrations/2026_07_09_131838_create_interactions_table.php b/database/migrations/2026_07_09_131838_create_interactions_table.php new file mode 100644 index 0000000..f175c7d --- /dev/null +++ b/database/migrations/2026_07_09_131838_create_interactions_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignIdFor(\App\Models\Application::class); + $table->foreignIdFor(\App\Models\Contact::class) + ->nullable() + ->comment('not every moment has a person'); + $table->string('type'); + $table->timestamp('occurred_at'); + $table->text('body')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('interactions'); + } +}; From 041c0350595776773f16880ea6ec4f5329c3b979 Mon Sep 17 00:00:00 2001 From: Tonko Mulder Date: Wed, 2 Sep 2026 17:45:42 +0200 Subject: [PATCH 3/4] Add factories for all models with relation and status/type states - ApplicationFactory: status states (lead/interviewing/offer/rejected/ withdrawn) and forCompany(); status_change is deliberately not a factory state, that Interaction type is observer-only per the v1 spec - CompanyFactory, ContactFactory (forCompany()), InteractionFactory (email/call/interview states, forApplication()/forContact()) - Annotate HasFactory with @use HasFactory on each model so Larastan's generic-trait check passes --- app/Models/Application.php | 5 ++ app/Models/Company.php | 6 ++- app/Models/Contact.php | 5 ++ app/Models/Interaction.php | 5 ++ database/factories/ApplicationFactory.php | 64 +++++++++++++++++++++++ database/factories/CompanyFactory.php | 32 ++++++++++++ database/factories/ContactFactory.php | 37 +++++++++++++ database/factories/InteractionFactory.php | 60 +++++++++++++++++++++ 8 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 database/factories/ApplicationFactory.php create mode 100644 database/factories/CompanyFactory.php create mode 100644 database/factories/ContactFactory.php create mode 100644 database/factories/InteractionFactory.php diff --git a/app/Models/Application.php b/app/Models/Application.php index 91f134b..d576772 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -5,6 +5,8 @@ namespace App\Models; use App\Enums\ApplicationStatus; +use Database\Factories\ApplicationFactory; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -24,6 +26,9 @@ */ final class Application extends Model { + /** @use HasFactory */ + use HasFactory; + protected $fillable = [ 'company_id', 'role_title', diff --git a/app/Models/Company.php b/app/Models/Company.php index 4181659..bc91af6 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -4,9 +4,10 @@ namespace App\Models; +use Database\Factories\CompanyFactory; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Testing\Fluent\Concerns\Has; /** * @property int $id @@ -24,6 +25,9 @@ */ final class Company extends Model { + /** @use HasFactory */ + use HasFactory; + protected $fillable = [ 'name', 'kvk_number', diff --git a/app/Models/Contact.php b/app/Models/Contact.php index d40690f..c7d244b 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -4,6 +4,8 @@ namespace App\Models; +use Database\Factories\ContactFactory; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -22,6 +24,9 @@ */ final class Contact extends Model { + /** @use HasFactory */ + use HasFactory; + protected $fillable = [ 'company_id', 'name', diff --git a/app/Models/Interaction.php b/app/Models/Interaction.php index 102452c..34ea2ae 100644 --- a/app/Models/Interaction.php +++ b/app/Models/Interaction.php @@ -5,6 +5,8 @@ namespace App\Models; use App\Enums\InteractionType; +use Database\Factories\InteractionFactory; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -22,6 +24,9 @@ */ final class Interaction extends Model { + /** @use HasFactory */ + use HasFactory; + protected $fillable = [ 'application_id', 'contact_id', diff --git a/database/factories/ApplicationFactory.php b/database/factories/ApplicationFactory.php new file mode 100644 index 0000000..6e0ff99 --- /dev/null +++ b/database/factories/ApplicationFactory.php @@ -0,0 +1,64 @@ + + */ +final class ApplicationFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'company_id' => Company::factory(), + 'role_title' => $this->faker->jobTitle(), + 'source' => null, + 'status' => ApplicationStatus::APPLIED, + 'applied_at' => null, + 'notes' => null, + ]; + } + + public function lead(): self + { + return $this->set('status', ApplicationStatus::LEAD); + } + + public function interviewing(): self + { + return $this->set('status', ApplicationStatus::INTERVIEWING); + } + + public function offer(): self + { + return $this->set('status', ApplicationStatus::OFFER); + } + + public function rejected(): self + { + return $this->set('status', ApplicationStatus::REJECTED); + } + + public function withdrawn(): self + { + return $this->set('status', ApplicationStatus::WITHDRAWN); + } + + /** @param Company|Factory $company */ + public function forCompany(Company|Factory $company): self + { + return $this->set('company_id', $company); + } +} diff --git a/database/factories/CompanyFactory.php b/database/factories/CompanyFactory.php new file mode 100644 index 0000000..72d3788 --- /dev/null +++ b/database/factories/CompanyFactory.php @@ -0,0 +1,32 @@ + + */ +final class CompanyFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => $this->faker->company(), + 'kvk_number' => null, + 'website' => null, + 'city' => null, + 'sbi_code' => null, + 'sbi_description' => null, + 'enriched_at' => null, + ]; + } +} diff --git a/database/factories/ContactFactory.php b/database/factories/ContactFactory.php new file mode 100644 index 0000000..5a8fce0 --- /dev/null +++ b/database/factories/ContactFactory.php @@ -0,0 +1,37 @@ + + */ +final class ContactFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'company_id' => Company::factory(), + 'name' => $this->faker->name(), + 'role' => null, + 'email' => null, + 'phone' => null, + ]; + } + + /** @param Company|Factory $company */ + public function forCompany(Company|Factory $company): self + { + return $this->set('company_id', $company); + } +} diff --git a/database/factories/InteractionFactory.php b/database/factories/InteractionFactory.php new file mode 100644 index 0000000..1eaf4c1 --- /dev/null +++ b/database/factories/InteractionFactory.php @@ -0,0 +1,60 @@ + + */ +final class InteractionFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'application_id' => Application::factory(), + 'contact_id' => null, + 'type' => InteractionType::NOTE, + 'occurred_at' => $this->faker->dateTimeBetween('-30 days'), + 'body' => $this->faker->sentence(), + ]; + } + + public function email(): self + { + return $this->set('type', InteractionType::EMAIL); + } + + public function call(): self + { + return $this->set('type', InteractionType::CALL); + } + + public function interview(): self + { + return $this->set('type', InteractionType::INTERVIEW); + } + + /** @param Application|Factory $application */ + public function forApplication(Application|Factory $application): self + { + return $this->set('application_id', $application); + } + + /** @param Contact|Factory $contact */ + public function forContact(Contact|Factory $contact): self + { + return $this->set('contact_id', $contact); + } +} From 59082cc4b2ea69499c1e00a474ffc434cf2bcbf2 Mon Sep 17 00:00:00 2001 From: Tonko Mulder Date: Thu, 3 Sep 2026 10:30:09 +0200 Subject: [PATCH 4/4] style: fix Pint errors (imports, spacing, migration class refs) --- app/Enums/InteractionType.php | 2 +- app/Models/Application.php | 11 +++++++---- app/Models/Company.php | 13 ++++++++----- app/Models/Contact.php | 8 +++++--- app/Models/Interaction.php | 8 +++++--- .../2026_07_09_131728_create_applications_table.php | 3 ++- .../2026_07_09_131821_create_contacts_table.php | 3 ++- .../2026_07_09_131838_create_interactions_table.php | 6 ++++-- 8 files changed, 34 insertions(+), 20 deletions(-) diff --git a/app/Enums/InteractionType.php b/app/Enums/InteractionType.php index d049c9e..efd52a0 100644 --- a/app/Enums/InteractionType.php +++ b/app/Enums/InteractionType.php @@ -7,7 +7,7 @@ enum InteractionType: string { case NOTE = 'note'; - case STATUS_CHANGE = 'status_change'; + case STATUS_CHANGE = 'status_change'; case EMAIL = 'email'; case CALL = 'call'; case INTERVIEW = 'interview'; diff --git a/app/Models/Application.php b/app/Models/Application.php index d576772..86565db 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -5,11 +5,14 @@ namespace App\Models; use App\Enums\ApplicationStatus; +use Carbon\CarbonImmutable; use Database\Factories\ApplicationFactory; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Carbon; /** * @property int $id @@ -17,12 +20,12 @@ * @property string $role_title * @property string|null $source * @property ApplicationStatus $status - * @property \Carbon\CarbonImmutable|null $applied_at + * @property CarbonImmutable|null $applied_at * @property string|null $notes - * @property \Illuminate\Support\Carbon $created_at - * @property \Illuminate\Support\Carbon $updated_at + * @property Carbon $created_at + * @property Carbon $updated_at * @property-read Company $company - * @property-read \Illuminate\Database\Eloquent\Collection $interactions + * @property-read Collection $interactions */ final class Application extends Model { diff --git a/app/Models/Company.php b/app/Models/Company.php index bc91af6..9771476 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -4,10 +4,13 @@ namespace App\Models; +use Carbon\CarbonImmutable; use Database\Factories\CompanyFactory; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Carbon; /** * @property int $id @@ -17,11 +20,11 @@ * @property string|null $city * @property string|null $sbi_code * @property string|null $sbi_description - * @property \Carbon\CarbonImmutable|null $enriched_at - * @property \Illuminate\Support\Carbon $created_at - * @property \Illuminate\Support\Carbon $updated_at - * @property-read \Illuminate\Database\Eloquent\Collection $applications - * @property-read \Illuminate\Database\Eloquent\Collection $contacts + * @property CarbonImmutable|null $enriched_at + * @property Carbon $created_at + * @property Carbon $updated_at + * @property-read Collection $applications + * @property-read Collection $contacts */ final class Company extends Model { diff --git a/app/Models/Contact.php b/app/Models/Contact.php index c7d244b..8e4bc11 100644 --- a/app/Models/Contact.php +++ b/app/Models/Contact.php @@ -5,10 +5,12 @@ namespace App\Models; use Database\Factories\ContactFactory; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Carbon; /** * @property int $id @@ -17,10 +19,10 @@ * @property string|null $role * @property string|null $email * @property string|null $phone - * @property \Illuminate\Support\Carbon $created_at - * @property \Illuminate\Support\Carbon $updated_at + * @property Carbon $created_at + * @property Carbon $updated_at * @property-read Company $company - * @property-read \Illuminate\Database\Eloquent\Collection $interactions + * @property-read Collection $interactions */ final class Contact extends Model { diff --git a/app/Models/Interaction.php b/app/Models/Interaction.php index 34ea2ae..5298604 100644 --- a/app/Models/Interaction.php +++ b/app/Models/Interaction.php @@ -5,20 +5,22 @@ namespace App\Models; use App\Enums\InteractionType; +use Carbon\CarbonImmutable; use Database\Factories\InteractionFactory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Carbon; /** * @property int $id * @property int $application_id * @property int|null $contact_id * @property InteractionType $type - * @property \Carbon\CarbonImmutable $occurred_at + * @property CarbonImmutable $occurred_at * @property string|null $body - * @property \Illuminate\Support\Carbon $created_at - * @property \Illuminate\Support\Carbon $updated_at + * @property Carbon $created_at + * @property Carbon $updated_at * @property-read Application $application * @property-read Contact|null $contact */ diff --git a/database/migrations/2026_07_09_131728_create_applications_table.php b/database/migrations/2026_07_09_131728_create_applications_table.php index 24df413..732210c 100644 --- a/database/migrations/2026_07_09_131728_create_applications_table.php +++ b/database/migrations/2026_07_09_131728_create_applications_table.php @@ -1,5 +1,6 @@ id(); - $table->foreignIdFor(\App\Models\Company::class); + $table->foreignIdFor(Company::class); $table->string('role_title') ->comment('e.g. "Medior Laravel Developer"'); $table->string('source') diff --git a/database/migrations/2026_07_09_131821_create_contacts_table.php b/database/migrations/2026_07_09_131821_create_contacts_table.php index f8d8587..d70df9b 100644 --- a/database/migrations/2026_07_09_131821_create_contacts_table.php +++ b/database/migrations/2026_07_09_131821_create_contacts_table.php @@ -1,5 +1,6 @@ id(); - $table->foreignIdFor(\App\Models\Company::class); + $table->foreignIdFor(Company::class); $table->string('name'); $table->string('role')->nullable(); $table->string('email')->nullable(); diff --git a/database/migrations/2026_07_09_131838_create_interactions_table.php b/database/migrations/2026_07_09_131838_create_interactions_table.php index f175c7d..b085a4b 100644 --- a/database/migrations/2026_07_09_131838_create_interactions_table.php +++ b/database/migrations/2026_07_09_131838_create_interactions_table.php @@ -1,5 +1,7 @@ id(); - $table->foreignIdFor(\App\Models\Application::class); - $table->foreignIdFor(\App\Models\Contact::class) + $table->foreignIdFor(Application::class); + $table->foreignIdFor(Contact::class) ->nullable() ->comment('not every moment has a person'); $table->string('type');