'array', 'is_active' => 'boolean', 'depth' => 'integer', ]; // Relationships public function parent(): BelongsTo { return $this->belongsTo(self::class, 'parent_id'); } public function children(): HasMany { return $this->hasMany(self::class, 'parent_id'); } public function workspace(): BelongsTo { return $this->belongsTo(Workspace::class); } public function permissions(): HasMany { return $this->hasMany(PermissionMatrix::class, 'entity_id'); } public function permissionRequests(): HasMany { return $this->hasMany(PermissionRequest::class, 'entity_id'); } // Type helpers public function isMaster(): bool { return $this->type === self::TYPE_M1_MASTER; } public function isFacade(): bool { return $this->type === self::TYPE_M2_FACADE; } public function isDropshipper(): bool { return $this->type === self::TYPE_M3_DROPSHIP; } // Hierarchy methods /** * Get ancestors from root to parent (not including self). */ public function getAncestors(): Collection { if (! $this->path || $this->depth === 0) { return collect(); } $pathCodes = explode('/', trim($this->path, '/')); array_pop($pathCodes); // Remove self if (empty($pathCodes)) { return collect(); } return static::whereIn('code', $pathCodes) ->orderBy('depth') ->get(); } /** * Get hierarchy from root to this entity (including self). */ public function getHierarchy(): Collection { $ancestors = $this->getAncestors(); $ancestors->push($this); return $ancestors; } /** * Get all descendants of this entity. */ public function getDescendants(): Collection { return static::where('path', 'like', $this->path.'/%')->get(); } /** * Get the root M1 entity for this hierarchy. */ public function getRoot(): self { if ($this->depth === 0) { return $this; } $rootCode = explode('/', trim($this->path, '/'))[0]; return static::where('code', $rootCode)->firstOrFail(); } // SKU methods /** * Generate SKU prefix for this entity. * Format: M1-M2-SKU or M1-M2-M3-SKU */ public function getSkuPrefix(): string { $pathCodes = explode('/', trim($this->path, '/')); return implode('-', $pathCodes); } /** * Build a full SKU with entity lineage. */ public function buildSku(string $baseSku): string { return $this->getSkuPrefix().'-'.$baseSku; } // Factory methods /** * Create a new M1 master entity. */ public static function createMaster(string $code, string $name, array $attributes = []): self { $code = Str::upper($code); return static::create(array_merge([ 'code' => $code, 'name' => $name, 'type' => self::TYPE_M1_MASTER, 'path' => $code, 'depth' => 0, ], $attributes)); } /** * Create a child entity under this one. */ public function createChild(string $code, string $name, string $type, array $attributes = []): self { $code = Str::upper($code); return static::create(array_merge([ 'code' => $code, 'name' => $name, 'type' => $type, 'parent_id' => $this->id, 'path' => $this->path.'/'.$code, 'depth' => $this->depth + 1, ], $attributes)); } /** * Create an M2 facade under this entity. */ public function createFacade(string $code, string $name, array $attributes = []): self { return $this->createChild($code, $name, self::TYPE_M2_FACADE, $attributes); } /** * Create an M3 dropshipper under this entity. */ public function createDropshipper(string $code, string $name, array $attributes = []): self { return $this->createChild($code, $name, self::TYPE_M3_DROPSHIP, $attributes); } // Type alias helpers public function isM1(): bool { return $this->isMaster(); } public function isM2(): bool { return $this->isFacade(); } public function isM3(): bool { return $this->isDropshipper(); } // Boot protected static function boot(): void { parent::boot(); static::creating(function (self $entity) { // Uppercase the code $entity->code = Str::upper($entity->code); // Compute path and depth if not set if (! $entity->path) { if ($entity->parent_id) { $parent = static::find($entity->parent_id); if ($parent) { $entity->path = $parent->path.'/'.$entity->code; $entity->depth = $parent->depth + 1; } } else { $entity->path = $entity->code; $entity->depth = 0; } } // Auto-determine type based on parent if not set if (! $entity->type) { if (! $entity->parent_id) { $entity->type = self::TYPE_M1_MASTER; } else { $parent = static::find($entity->parent_id); $entity->type = match ($parent?->type) { self::TYPE_M1_MASTER => self::TYPE_M2_FACADE, self::TYPE_M2_FACADE => self::TYPE_M3_DROPSHIP, default => self::TYPE_M3_DROPSHIP, }; } } }); } // Scopes public function scopeActive($query) { return $query->where('is_active', true); } public function scopeMasters($query) { return $query->where('type', self::TYPE_M1_MASTER); } public function scopeFacades($query) { return $query->where('type', self::TYPE_M2_FACADE); } public function scopeDropshippers($query) { return $query->where('type', self::TYPE_M3_DROPSHIP); } public function scopeForWorkspace($query, int $workspaceId) { return $query->where('workspace_id', $workspaceId); } // Settings helpers public function getSetting(string $key, $default = null) { return data_get($this->settings, $key, $default); } public function setSetting(string $key, $value): self { $settings = $this->settings ?? []; data_set($settings, $key, $value); $this->settings = $settings; return $this; } }