Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions app-modules/profile/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "he4rt/profile",
"description": "Tenant-scoped member profile module",
"type": "library",
"version": "1.0",
"license": "proprietary",
"require": {},
"autoload": {
"psr-4": {
"He4rt\\Profile\\": "src/",
"He4rt\\Profile\\Database\\Factories\\": "database/factories/",
"He4rt\\Profile\\Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"He4rt\\Profile\\Tests\\": "tests/"
}
},
"minimum-stability": "stable",
"extra": {
"laravel": {
"providers": [
"He4rt\\Profile\\ProfileServiceProvider"
]
}
}
}
56 changes: 56 additions & 0 deletions app-modules/profile/database/factories/ProfileFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace He4rt\Profile\Database\Factories;

use He4rt\Identity\Tenant\Models\Tenant;
use He4rt\Identity\User\Models\User;
use He4rt\Profile\Enums\SeniorityLevel;
use He4rt\Profile\Enums\SocialPlatform;
use He4rt\Profile\Enums\StartAvailability;
use He4rt\Profile\Models\Profile;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
* @extends Factory<Profile>
*/
final class ProfileFactory extends Factory
{
protected $model = Profile::class;

public function definition(): array
{
return [
'user_id' => User::factory(),
'tenant_id' => Tenant::factory(),
'nickname' => null,
'birthdate' => null,
'about' => null,
'headline' => null,
'seniority_level' => null,
'years_experience' => null,
'social_links' => null,
'available_for_proposals' => false,
'start_availability' => null,
];
}

public function complete(): self
{
return $this->state(fn (): array => [
'nickname' => fake()->userName(),
'birthdate' => fake()->date(),
'about' => fake()->paragraph(),
'headline' => fake()->jobTitle(),
'seniority_level' => fake()->randomElement(SeniorityLevel::cases()),
'years_experience' => fake()->numberBetween(1, 20),
'social_links' => [
SocialPlatform::Instagram->value => fake()->userName(),
SocialPlatform::Website->value => fake()->url(),
],
'available_for_proposals' => true,
'start_availability' => fake()->randomElement(StartAvailability::cases()),
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::create('user_profiles', function (Blueprint $table): void {
$table->uuid('id')->primary();
$table->foreignUuid('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('tenant_id')->constrained('tenants')->cascadeOnDelete();
$table->string('nickname')->nullable();
$table->date('birthdate')->nullable();
$table->text('about')->nullable();
$table->string('headline')->nullable();
$table->string('seniority_level', 30)->nullable();
$table->smallInteger('years_experience')->nullable();
$table->jsonb('social_links')->nullable();
$table->boolean('available_for_proposals')->default(false);
$table->string('start_availability', 30)->nullable();
$table->timestamps();

$table->unique(['user_id', 'tenant_id']);
});

DB::statement('CREATE INDEX user_profiles_available_for_proposals_index ON user_profiles (tenant_id, user_id) WHERE available_for_proposals = true');
}

public function down(): void
{
Schema::dropIfExists('user_profiles');
}
};
2 changes: 2 additions & 0 deletions app-modules/profile/phpstan.ignore.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
parameters:
ignoreErrors: []
6 changes: 6 additions & 0 deletions app-modules/profile/phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
includes:
- phpstan.ignore.neon

parameters:
paths:
- src/
25 changes: 25 additions & 0 deletions app-modules/profile/src/Enums/SeniorityLevel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace He4rt\Profile\Enums;

enum SeniorityLevel: string
{
case Junior = 'junior';
case Pleno = 'pleno';
case Senior = 'senior';
case Specialist = 'specialist';
case Lead = 'lead';

public function label(): string
{
return match ($this) {
self::Junior => 'Júnior',
self::Pleno => 'Pleno',
self::Senior => 'Sênior',
self::Specialist => 'Especialista',
self::Lead => 'Lead',
};
}
}
36 changes: 36 additions & 0 deletions app-modules/profile/src/Enums/SocialPlatform.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace He4rt\Profile\Enums;

enum SocialPlatform: string
{
case Instagram = 'instagram';
case Twitter = 'twitter';
case Website = 'website';
case YouTube = 'youtube';
case Bluesky = 'bluesky';

public function label(): string
{
return match ($this) {
self::Instagram => 'Instagram',
self::Twitter => 'Twitter',
self::Website => 'Website',
self::YouTube => 'YouTube',
self::Bluesky => 'Bluesky',
};
}

/**
* @return array<int, string>
*/
public static function values(): array
{
return array_map(
fn (self $platform): string => $platform->value,
self::cases(),
);
}
}
29 changes: 29 additions & 0 deletions app-modules/profile/src/Enums/StartAvailability.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace He4rt\Profile\Enums;

enum StartAvailability: string
{
case Immediate = 'immediate';
case OneWeek = '1_week';
case TwoWeeks = '2_weeks';
case ThreeWeeks = '3_weeks';
case OneMonth = '1_month';
case TwoMonths = '2_months';
case Negotiable = 'negotiable';

public function label(): string
{
return match ($this) {
self::Immediate => 'Imediato',
self::OneWeek => '1 semana',
self::TwoWeeks => '2 semanas',
self::ThreeWeeks => '3 semanas',
self::OneMonth => '1 mês',
self::TwoMonths => '2 meses',
self::Negotiable => 'Negociável',
};
}
}
20 changes: 20 additions & 0 deletions app-modules/profile/src/Listeners/CreateProfileForTenantMember.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace He4rt\Profile\Listeners;

use He4rt\Profile\Models\Profile;

final class CreateProfileForTenantMember
{
public function handle(string $userId, int $tenantId): void
{
Profile::query()->firstOrCreate([
'user_id' => $userId,
'tenant_id' => $tenantId,
], [
'available_for_proposals' => false,
]);
}
}
102 changes: 102 additions & 0 deletions app-modules/profile/src/Models/Profile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

namespace He4rt\Profile\Models;

use He4rt\Identity\Tenant\Models\Tenant;
use He4rt\Identity\User\Models\User;
use He4rt\Profile\Database\Factories\ProfileFactory;
use He4rt\Profile\Enums\SeniorityLevel;
use He4rt\Profile\Enums\SocialPlatform;
use He4rt\Profile\Enums\StartAvailability;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use InvalidArgumentException;

/**
* @property string $id
* @property string $user_id
* @property int $tenant_id
* @property array<string, string>|null $social_links
*/
final class Profile extends Model
{
/** @use HasFactory<ProfileFactory> */
use HasFactory;
use HasUuids;

protected $table = 'user_profiles';

protected $fillable = [
'user_id',
'tenant_id',
'nickname',
'birthdate',
'about',
'headline',
'seniority_level',
'years_experience',
'social_links',
'available_for_proposals',
'start_availability',
];

/**
* @return BelongsTo<User, $this>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

/**
* @return BelongsTo<Tenant, $this>
*/
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}

protected static function newFactory(): ProfileFactory
{
return ProfileFactory::new();
}

/**
* @return Attribute<array<string, string>|null, array<string, string>|null>
*/
protected function socialLinks(): Attribute
{
return Attribute::set(function (?array $value): ?string {
if ($value === null) {
return null;
}

$invalidPlatforms = array_diff(array_keys($value), SocialPlatform::values());

if ($invalidPlatforms !== []) {
throw new InvalidArgumentException(sprintf(
'Invalid social platform keys: %s.',
implode(', ', $invalidPlatforms),
));
}

return json_encode($value, JSON_THROW_ON_ERROR);
});
}

protected function casts(): array
{
return [
'birthdate' => 'date',
'social_links' => 'array',
'available_for_proposals' => 'boolean',
'seniority_level' => SeniorityLevel::class,
'start_availability' => StartAvailability::class,
];
}
}
Loading