feat: implement tenant management system including CRUD operations and domain bootstrapping functionality
This commit is contained in:
@@ -19,10 +19,7 @@ class TenantController extends Controller
|
|||||||
|
|
||||||
public function store(StoreTenantRequest $request): JsonResponse
|
public function store(StoreTenantRequest $request): JsonResponse
|
||||||
{
|
{
|
||||||
$validated = $request->validated();
|
$tenant = Tenant::query()->create($request->validated());
|
||||||
$props = $validated['props'] ?? [];
|
|
||||||
|
|
||||||
$tenant = Tenant::createWithProps($validated, $props);
|
|
||||||
|
|
||||||
return TenantResource::make($tenant)->response()->setStatusCode(201);
|
return TenantResource::make($tenant)->response()->setStatusCode(201);
|
||||||
}
|
}
|
||||||
@@ -34,10 +31,7 @@ class TenantController extends Controller
|
|||||||
|
|
||||||
public function update(UpdateTenantRequest $request, Tenant $tenant): TenantResource
|
public function update(UpdateTenantRequest $request, Tenant $tenant): TenantResource
|
||||||
{
|
{
|
||||||
$validated = $request->validated();
|
$tenant->update($request->validated());
|
||||||
$props = $validated['props'] ?? [];
|
|
||||||
|
|
||||||
$tenant->updateWithProps($validated, $props);
|
|
||||||
|
|
||||||
return TenantResource::make($tenant);
|
return TenantResource::make($tenant);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Tenant\Controllers;
|
|
||||||
|
|
||||||
use App\Domains\Tenant\Models\TenantProp;
|
|
||||||
use App\Domains\Tenant\Requests\StoreTenantPropRequest;
|
|
||||||
use App\Domains\Tenant\Requests\UpdateTenantPropRequest;
|
|
||||||
use App\Domains\Tenant\Resources\TenantPropResource;
|
|
||||||
use App\Http\Controllers\Controller;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
|
||||||
use Illuminate\Http\Response;
|
|
||||||
|
|
||||||
class TenantPropController extends Controller
|
|
||||||
{
|
|
||||||
public function index(): JsonResponse
|
|
||||||
{
|
|
||||||
return TenantPropResource::collection(TenantProp::query()->latest()->get())->response();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function store(StoreTenantPropRequest $request): JsonResponse
|
|
||||||
{
|
|
||||||
$tenantProp = TenantProp::query()->create($request->validated());
|
|
||||||
|
|
||||||
return TenantPropResource::make($tenantProp)->response()->setStatusCode(201);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function show(TenantProp $tenantProp): TenantPropResource
|
|
||||||
{
|
|
||||||
return TenantPropResource::make($tenantProp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function update(UpdateTenantPropRequest $request, TenantProp $tenantProp): TenantPropResource
|
|
||||||
{
|
|
||||||
$tenantProp->update($request->validated());
|
|
||||||
|
|
||||||
return TenantPropResource::make($tenantProp);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function destroy(TenantProp $tenantProp): Response
|
|
||||||
{
|
|
||||||
$tenantProp->delete();
|
|
||||||
|
|
||||||
return response()->noContent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,8 +8,6 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use LogicException;
|
|
||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'codigo',
|
'codigo',
|
||||||
@@ -21,27 +19,6 @@ class Tenant extends Model
|
|||||||
use HasAttachments;
|
use HasAttachments;
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $attributes
|
|
||||||
* @param array<string, mixed> $props
|
|
||||||
* @return static
|
|
||||||
*/
|
|
||||||
public static function createWithProps(array $attributes, array $props = []): static
|
|
||||||
{
|
|
||||||
unset($attributes['props']);
|
|
||||||
|
|
||||||
/** @var static $tenant */
|
|
||||||
$tenant = DB::transaction(function () use ($attributes, $props): self {
|
|
||||||
/** @var static $createdTenant */
|
|
||||||
$createdTenant = static::query()->create($attributes);
|
|
||||||
$createdTenant->syncPropValues($props);
|
|
||||||
|
|
||||||
return $createdTenant;
|
|
||||||
});
|
|
||||||
|
|
||||||
return $tenant;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HasMany<Product, $this>
|
* @return HasMany<Product, $this>
|
||||||
*/
|
*/
|
||||||
@@ -49,77 +26,5 @@ class Tenant extends Model
|
|||||||
{
|
{
|
||||||
return $this->hasMany(Product::class, 'tenant_codigo', 'codigo');
|
return $this->hasMany(Product::class, 'tenant_codigo', 'codigo');
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* @return HasMany<TenantPropValue, $this>
|
|
||||||
*/
|
|
||||||
public function propValues(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(TenantPropValue::class, 'tenant_codigo', 'codigo');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPropValue(TenantProp|string $prop): ?TenantPropValue
|
|
||||||
{
|
|
||||||
$resolvedProp = $this->resolveProp($prop);
|
|
||||||
|
|
||||||
return $this->propValues()
|
|
||||||
->where('tenant_prop_codigo', $resolvedProp->codigo)
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setPropValue(TenantProp|string $prop, mixed $value): TenantPropValue
|
|
||||||
{
|
|
||||||
$this->ensurePropValuesCanBeManaged();
|
|
||||||
|
|
||||||
$resolvedProp = $this->resolveProp($prop);
|
|
||||||
|
|
||||||
return $this->propValues()->updateOrCreate(
|
|
||||||
['tenant_prop_codigo' => $resolvedProp->codigo],
|
|
||||||
['value' => $value],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $props
|
|
||||||
*/
|
|
||||||
public function syncPropValues(array $props): void
|
|
||||||
{
|
|
||||||
foreach ($props as $codigo => $value) {
|
|
||||||
$this->setPropValue((string) $codigo, $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $attributes
|
|
||||||
* @param array<string, mixed> $props
|
|
||||||
* @return static
|
|
||||||
*/
|
|
||||||
public function updateWithProps(array $attributes, array $props = []): static
|
|
||||||
{
|
|
||||||
unset($attributes['props']);
|
|
||||||
|
|
||||||
DB::transaction(function () use ($attributes, $props): void {
|
|
||||||
$this->update($attributes);
|
|
||||||
$this->syncPropValues($props);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function ensurePropValuesCanBeManaged(): void
|
|
||||||
{
|
|
||||||
if (! $this->exists) {
|
|
||||||
throw new LogicException('Cannot manage prop values for an unsaved tenant.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function resolveProp(TenantProp|string $prop): TenantProp
|
|
||||||
{
|
|
||||||
if ($prop instanceof TenantProp) {
|
|
||||||
return $prop;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TenantProp::query()
|
|
||||||
->where('codigo', $prop)
|
|
||||||
->firstOrFail();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Tenant\Models;
|
|
||||||
|
|
||||||
use App\Domains\Shared\Enums\FieldType;
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
||||||
|
|
||||||
#[Fillable([
|
|
||||||
'codigo',
|
|
||||||
'nombre',
|
|
||||||
'descripcion',
|
|
||||||
'is_required',
|
|
||||||
'data_type',
|
|
||||||
])]
|
|
||||||
class TenantProp extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $table = 'tenant_props';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
protected function casts(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'is_required' => 'boolean',
|
|
||||||
'data_type' => FieldType::class,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return HasMany<TenantPropValue, $this>
|
|
||||||
*/
|
|
||||||
public function values(): HasMany
|
|
||||||
{
|
|
||||||
return $this->hasMany(TenantPropValue::class, 'tenant_prop_codigo', 'codigo');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Tenant\Models;
|
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
||||||
|
|
||||||
#[Fillable([
|
|
||||||
'tenant_codigo',
|
|
||||||
'tenant_prop_codigo',
|
|
||||||
'value',
|
|
||||||
])]
|
|
||||||
class TenantPropValue extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
protected $table = 'tenant_prop_values';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return BelongsTo<Tenant, $this>
|
|
||||||
*/
|
|
||||||
public function tenant(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return BelongsTo<TenantProp, $this>
|
|
||||||
*/
|
|
||||||
public function prop(): BelongsTo
|
|
||||||
{
|
|
||||||
return $this->belongsTo(TenantProp::class, 'tenant_prop_codigo', 'codigo');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Tenant\Requests;
|
|
||||||
|
|
||||||
use App\Domains\Shared\Enums\FieldType;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class StoreTenantPropRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'codigo' => ['required', 'string', 'max:255', Rule::unique('tenant_props', 'codigo')],
|
|
||||||
'nombre' => ['required', 'string', 'max:255'],
|
|
||||||
'descripcion' => ['nullable', 'string'],
|
|
||||||
'is_required' => ['sometimes', 'boolean'],
|
|
||||||
'data_type' => ['required', Rule::enum(FieldType::class)],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Domains\Tenant\Requests;
|
namespace App\Domains\Tenant\Requests;
|
||||||
|
|
||||||
use App\Domains\Tenant\Support\TenantPropRules;
|
|
||||||
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
@@ -50,7 +49,6 @@ class StoreTenantRequest extends FormRequest
|
|||||||
'max:255',
|
'max:255',
|
||||||
Rule::unique('tenants', 'dominio'),
|
Rule::unique('tenants', 'dominio'),
|
||||||
],
|
],
|
||||||
...TenantPropRules::sync(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Tenant\Requests;
|
|
||||||
|
|
||||||
use App\Domains\Tenant\Models\TenantProp;
|
|
||||||
use App\Domains\Shared\Enums\FieldType;
|
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
|
||||||
use Illuminate\Validation\Rule;
|
|
||||||
|
|
||||||
class UpdateTenantPropRequest extends FormRequest
|
|
||||||
{
|
|
||||||
public function authorize(): bool
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function rules(): array
|
|
||||||
{
|
|
||||||
/** @var TenantProp|null $tenantProp */
|
|
||||||
$tenantProp = $this->route('tenantProp');
|
|
||||||
|
|
||||||
return [
|
|
||||||
'codigo' => ['required', 'string', 'max:255', Rule::unique('tenant_props', 'codigo')->ignore($tenantProp?->id)],
|
|
||||||
'nombre' => ['required', 'string', 'max:255'],
|
|
||||||
'descripcion' => ['nullable', 'string'],
|
|
||||||
'is_required' => ['sometimes', 'boolean'],
|
|
||||||
'data_type' => ['required', Rule::enum(FieldType::class)],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ namespace App\Domains\Tenant\Requests;
|
|||||||
|
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
use App\Domains\Tenant\Support\TenantDomainNormalizer;
|
||||||
use App\Domains\Tenant\Support\TenantPropRules;
|
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
use Illuminate\Validation\Rule;
|
use Illuminate\Validation\Rule;
|
||||||
@@ -59,7 +58,6 @@ class UpdateTenantRequest extends FormRequest
|
|||||||
'max:255',
|
'max:255',
|
||||||
Rule::unique('tenants', 'dominio')->ignore($tenant?->id),
|
Rule::unique('tenants', 'dominio')->ignore($tenant?->id),
|
||||||
],
|
],
|
||||||
...TenantPropRules::sync(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Tenant\Resources;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @mixin \App\Domains\Tenant\Models\TenantProp
|
|
||||||
*/
|
|
||||||
class TenantPropResource extends JsonResource
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public function toArray(Request $request): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'id' => $this->id,
|
|
||||||
'codigo' => $this->codigo,
|
|
||||||
'nombre' => $this->nombre,
|
|
||||||
'descripcion' => $this->descripcion,
|
|
||||||
'is_required' => $this->is_required,
|
|
||||||
'data_type' => $this->data_type?->value ?? $this->data_type,
|
|
||||||
'created_at' => $this->created_at,
|
|
||||||
'updated_at' => $this->updated_at,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,29 +10,6 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
|||||||
*/
|
*/
|
||||||
class TenantResource extends JsonResource
|
class TenantResource extends JsonResource
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
protected function collapseProps(): array
|
|
||||||
{
|
|
||||||
return $this->resource->propValues()
|
|
||||||
->with('prop')
|
|
||||||
->get()
|
|
||||||
->mapWithKeys(fn ($propValue): array => [
|
|
||||||
$propValue->tenant_prop_codigo => $this->normalizePropValue($propValue->value, $propValue->prop?->data_type?->value),
|
|
||||||
])
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function normalizePropValue(mixed $value, ?string $dataType): mixed
|
|
||||||
{
|
|
||||||
return match ($dataType) {
|
|
||||||
'boolean' => filter_var($value, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? $value,
|
|
||||||
'number' => is_numeric($value) ? $value + 0 : $value,
|
|
||||||
default => $value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
@@ -43,7 +20,6 @@ class TenantResource extends JsonResource
|
|||||||
'codigo' => $this->codigo,
|
'codigo' => $this->codigo,
|
||||||
'nombre' => $this->nombre,
|
'nombre' => $this->nombre,
|
||||||
'dominio' => $this->dominio,
|
'dominio' => $this->dominio,
|
||||||
'props' => $this->collapseProps(),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domains\Tenant\Support;
|
|
||||||
|
|
||||||
use App\Domains\Tenant\Models\TenantProp;
|
|
||||||
use Closure;
|
|
||||||
|
|
||||||
class TenantPropRules
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
public static function sync(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'props' => [
|
|
||||||
'sometimes',
|
|
||||||
'array',
|
|
||||||
static function (string $attribute, mixed $value, Closure $fail): void {
|
|
||||||
static::validateExistingProps($attribute, $value, $fail);
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'props.*' => ['nullable'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static function validateExistingProps(string $attribute, mixed $value, Closure $fail): void
|
|
||||||
{
|
|
||||||
if (! is_array($value) || $value === []) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$propCodes = array_map('strval', array_keys($value));
|
|
||||||
$existingPropCodes = TenantProp::query()
|
|
||||||
->whereIn('codigo', $propCodes)
|
|
||||||
->pluck('codigo')
|
|
||||||
->all();
|
|
||||||
|
|
||||||
$missingPropCodes = array_values(array_diff($propCodes, $existingPropCodes));
|
|
||||||
|
|
||||||
if ($missingPropCodes === []) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$fail(sprintf(
|
|
||||||
'The selected %s are invalid for Tenant: %s.',
|
|
||||||
$attribute,
|
|
||||||
implode(', ', $missingPropCodes),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,9 @@
|
|||||||
|
|
||||||
use App\Domains\Tenant\Controllers\BootstrapTenantController;
|
use App\Domains\Tenant\Controllers\BootstrapTenantController;
|
||||||
use App\Domains\Tenant\Controllers\TenantController;
|
use App\Domains\Tenant\Controllers\TenantController;
|
||||||
use App\Domains\Tenant\Controllers\TenantPropController;
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('tenants/bootstrap/{dominio}', BootstrapTenantController::class)
|
Route::get('tenants/bootstrap/{dominio}', BootstrapTenantController::class)
|
||||||
->where('dominio', '.*');
|
->where('dominio', '.*');
|
||||||
|
|
||||||
Route::apiResource('tenants', TenantController::class);
|
Route::apiResource('tenants', TenantController::class);
|
||||||
Route::apiResource('tenant-props', TenantPropController::class)
|
|
||||||
->parameters(['tenant-props' => 'tenantProp']);
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('tenant_props', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('codigo')->unique();
|
|
||||||
$table->string('nombre');
|
|
||||||
$table->text('descripcion')->nullable();
|
|
||||||
$table->boolean('is_required')->default(false);
|
|
||||||
$table->string('data_type');
|
|
||||||
$table->timestamps();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('tenant_props');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*/
|
|
||||||
public function up(): void
|
|
||||||
{
|
|
||||||
Schema::create('tenant_prop_values', function (Blueprint $table) {
|
|
||||||
$table->id();
|
|
||||||
$table->string('tenant_codigo');
|
|
||||||
$table->string('tenant_prop_codigo');
|
|
||||||
$table->text('value')->nullable();
|
|
||||||
$table->timestamps();
|
|
||||||
|
|
||||||
$table->foreign('tenant_codigo')
|
|
||||||
->references('codigo')
|
|
||||||
->on('tenants')
|
|
||||||
->cascadeOnUpdate()
|
|
||||||
->cascadeOnDelete();
|
|
||||||
|
|
||||||
$table->foreign('tenant_prop_codigo')
|
|
||||||
->references('codigo')
|
|
||||||
->on('tenant_props')
|
|
||||||
->cascadeOnUpdate()
|
|
||||||
->cascadeOnDelete();
|
|
||||||
|
|
||||||
$table->unique(['tenant_codigo', 'tenant_prop_codigo']);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*/
|
|
||||||
public function down(): void
|
|
||||||
{
|
|
||||||
Schema::dropIfExists('tenant_prop_values');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
namespace Tests\Feature\Tenant;
|
namespace Tests\Feature\Tenant;
|
||||||
|
|
||||||
use App\Domains\Tenant\Models\Tenant;
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
use App\Domains\Tenant\Models\TenantProp;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ class BootstrapTenantControllerTest extends TestCase
|
|||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
public function test_it_bootstraps_a_tenant_by_domain_and_includes_props(): void
|
public function test_it_bootstraps_a_tenant_by_domain(): void
|
||||||
{
|
{
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'codigo' => 'acme',
|
'codigo' => 'acme',
|
||||||
@@ -19,23 +18,14 @@ class BootstrapTenantControllerTest extends TestCase
|
|||||||
'dominio' => 'acme.com',
|
'dominio' => 'acme.com',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
TenantProp::create([
|
|
||||||
'codigo' => 'primary_color',
|
|
||||||
'nombre' => 'Primary Color',
|
|
||||||
'descripcion' => 'Brand color',
|
|
||||||
'is_required' => false,
|
|
||||||
'data_type' => 'string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tenant->setPropValue('primary_color', 'blue');
|
|
||||||
|
|
||||||
$response = $this->getJson('/api/tenants/bootstrap/acme.com');
|
$response = $this->getJson('/api/tenants/bootstrap/acme.com');
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('data.codigo', 'acme')
|
->assertJsonPath('data.codigo', 'acme')
|
||||||
->assertJsonPath('data.dominio', 'acme.com')
|
->assertJsonPath('data.dominio', 'acme.com');
|
||||||
->assertJsonPath('data.props.primary_color', 'blue');
|
|
||||||
|
$this->assertArrayNotHasKey('props', $response->json('data'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_bootstraps_a_tenant_from_a_full_url(): void
|
public function test_it_bootstraps_a_tenant_from_a_full_url(): void
|
||||||
@@ -65,33 +55,15 @@ class BootstrapTenantControllerTest extends TestCase
|
|||||||
|
|
||||||
public function test_it_rejects_duplicate_domains_after_normalization_when_storing(): void
|
public function test_it_rejects_duplicate_domains_after_normalization_when_storing(): void
|
||||||
{
|
{
|
||||||
TenantProp::create([
|
|
||||||
'codigo' => 'primary_color',
|
|
||||||
'nombre' => 'Primary Color',
|
|
||||||
'descripcion' => 'Brand color',
|
|
||||||
'is_required' => false,
|
|
||||||
'data_type' => 'string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$firstResponse = $this->postJson('/api/tenants', [
|
$firstResponse = $this->postJson('/api/tenants', [
|
||||||
'codigo' => 'acme',
|
'codigo' => 'acme',
|
||||||
'nombre' => 'Acme',
|
'nombre' => 'Acme',
|
||||||
'dominio' => 'https://ACME.com/path',
|
'dominio' => 'https://ACME.com/path',
|
||||||
'props' => [
|
|
||||||
'primary_color' => 'blue',
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$firstResponse
|
$firstResponse
|
||||||
->assertCreated()
|
->assertCreated()
|
||||||
->assertJsonPath('data.dominio', 'acme.com')
|
->assertJsonPath('data.dominio', 'acme.com');
|
||||||
->assertJsonPath('data.props.primary_color', 'blue');
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('tenant_prop_values', [
|
|
||||||
'tenant_codigo' => 'acme',
|
|
||||||
'tenant_prop_codigo' => 'primary_color',
|
|
||||||
'value' => 'blue',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$secondResponse = $this->postJson('/api/tenants', [
|
$secondResponse = $this->postJson('/api/tenants', [
|
||||||
'codigo' => 'globex',
|
'codigo' => 'globex',
|
||||||
@@ -106,14 +78,6 @@ class BootstrapTenantControllerTest extends TestCase
|
|||||||
|
|
||||||
public function test_it_allows_keeping_the_same_domain_on_update_but_rejects_collisions(): void
|
public function test_it_allows_keeping_the_same_domain_on_update_but_rejects_collisions(): void
|
||||||
{
|
{
|
||||||
TenantProp::create([
|
|
||||||
'codigo' => 'primary_color',
|
|
||||||
'nombre' => 'Primary Color',
|
|
||||||
'descripcion' => 'Brand color',
|
|
||||||
'is_required' => false,
|
|
||||||
'data_type' => 'string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$tenant = Tenant::create([
|
$tenant = Tenant::create([
|
||||||
'codigo' => 'acme',
|
'codigo' => 'acme',
|
||||||
'nombre' => 'Acme',
|
'nombre' => 'Acme',
|
||||||
@@ -130,16 +94,12 @@ class BootstrapTenantControllerTest extends TestCase
|
|||||||
'codigo' => 'acme',
|
'codigo' => 'acme',
|
||||||
'nombre' => 'Acme Updated',
|
'nombre' => 'Acme Updated',
|
||||||
'dominio' => 'https://ACME.com:443/admin',
|
'dominio' => 'https://ACME.com:443/admin',
|
||||||
'props' => [
|
|
||||||
'primary_color' => 'green',
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$successfulResponse
|
$successfulResponse
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('data.nombre', 'Acme Updated')
|
->assertJsonPath('data.nombre', 'Acme Updated')
|
||||||
->assertJsonPath('data.dominio', 'acme.com')
|
->assertJsonPath('data.dominio', 'acme.com');
|
||||||
->assertJsonPath('data.props.primary_color', 'green');
|
|
||||||
|
|
||||||
$failingResponse = $this->putJson("/api/tenants/{$otherTenant->id}", [
|
$failingResponse = $this->putJson("/api/tenants/{$otherTenant->id}", [
|
||||||
'codigo' => 'globex',
|
'codigo' => 'globex',
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Feature\Tenant;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
class TenantPropControllerTest extends TestCase
|
|
||||||
{
|
|
||||||
use RefreshDatabase;
|
|
||||||
|
|
||||||
public function test_it_creates_a_tenant_prop(): void
|
|
||||||
{
|
|
||||||
$response = $this->postJson('/api/tenant-props', [
|
|
||||||
'codigo' => 'primary_color',
|
|
||||||
'nombre' => 'Primary Color',
|
|
||||||
'descripcion' => 'Brand color',
|
|
||||||
'is_required' => false,
|
|
||||||
'data_type' => 'string',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertCreated()
|
|
||||||
->assertJsonPath('data.codigo', 'primary_color')
|
|
||||||
->assertJsonPath('data.data_type', 'string');
|
|
||||||
|
|
||||||
$this->assertDatabaseHas('tenant_props', [
|
|
||||||
'codigo' => 'primary_color',
|
|
||||||
'data_type' => 'string',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_it_rejects_unknown_data_type(): void
|
|
||||||
{
|
|
||||||
$response = $this->postJson('/api/tenant-props', [
|
|
||||||
'codigo' => 'primary_color',
|
|
||||||
'nombre' => 'Primary Color',
|
|
||||||
'data_type' => 'unsupported',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response
|
|
||||||
->assertUnprocessable()
|
|
||||||
->assertJsonValidationErrors(['data_type']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user