diff --git a/.env.example b/.env.example
index 2c7e32e24..294672dd0 100644
--- a/.env.example
+++ b/.env.example
@@ -9,7 +9,6 @@ APP_STORE_URL=http://localhost
APP_ADMIN_URL=http://admin.localhost
LOG_CHANNEL=stack
-L5_SWAGGER_GENERATE_ALWAYS=false
QUEUE_CONNECTION=redis
CACHE_DRIVER=redis
@@ -41,9 +40,6 @@ SILVERBOX_HOST=silverbox.localhost
SILVERBOX_CLIENT=heseya
SILVERBOX_KEY=secret
-FACEBOOK_APP_ID=
-FACEBOOK_APP_SECRET=
-
FURGONETKA_ENABLED=false
FURGONETKA_WEBHOOK_SALT=tajnasól
FURGONETKA_LOGIN=
diff --git a/app/Casts/MetadataValue.php b/app/Casts/MetadataValue.php
new file mode 100644
index 000000000..416859692
--- /dev/null
+++ b/app/Casts/MetadataValue.php
@@ -0,0 +1,44 @@
+value_type->value) {
+ MetadataType::BOOLEAN => (bool) $value,
+ MetadataType::NUMBER => (float) $value,
+ default => $value
+ };
+ }
+
+ /**
+ * Prepare the given value for storage.
+ *
+ * @param Model $model
+ * @param string $key
+ * @param mixed $value
+ * @param array $attributes
+ *
+ * @return mixed
+ */
+ public function set($model, string $key, $value, array $attributes)
+ {
+ return $value;
+ }
+}
diff --git a/app/SearchTypes/DiscountSearch.php b/app/Criteria/DiscountSearch.php
similarity index 81%
rename from app/SearchTypes/DiscountSearch.php
rename to app/Criteria/DiscountSearch.php
index eef7b9964..f76ef80ed 100644
--- a/app/SearchTypes/DiscountSearch.php
+++ b/app/Criteria/DiscountSearch.php
@@ -1,11 +1,11 @@
deniesAbilityByModel('show_metadata_private', $query->getModel())) {
+ return $query;
+ }
+
+ return $query->where(function (Builder $query): void {
+ $query->whereHas('metadataPrivate', function (Builder $query): void {
+ $first = true;
+ foreach ($this->value as $key => $value) {
+ if ($first) {
+ $query->where('name', 'LIKE', "%${key}%")
+ ->where('value', 'LIKE', "%${value}%");
+ $first = false;
+ } else {
+ $query->orWhere('name', 'LIKE', "%${key}%")
+ ->where('value', 'LIKE', "%${value}%");
+ }
+ }
+ });
+ });
+ }
+}
diff --git a/app/Criteria/MetadataSearch.php b/app/Criteria/MetadataSearch.php
new file mode 100644
index 000000000..356f9405c
--- /dev/null
+++ b/app/Criteria/MetadataSearch.php
@@ -0,0 +1,26 @@
+value as $key => $value) {
+ $query->whereHas($relation, function (Builder $query) use ($key, $value): void {
+ $query->where('name', '=', $key)
+ ->where('value', '=', $value);
+ });
+ }
+
+ return $query;
+ }
+
+ public function query(Builder $query): Builder
+ {
+ return $this->makeQuery($query, 'metadata');
+ }
+}
diff --git a/app/SearchTypes/OrderSearch.php b/app/Criteria/OrderSearch.php
similarity index 96%
rename from app/SearchTypes/OrderSearch.php
rename to app/Criteria/OrderSearch.php
index f24fadf2a..dc6b8e6a2 100644
--- a/app/SearchTypes/OrderSearch.php
+++ b/app/Criteria/OrderSearch.php
@@ -1,11 +1,11 @@
where(function (Builder $query): void {
+ $query->whereHas('status', function (Builder $query) {
+ return $query->where('hidden', $this->value);
+ });
+
+ if (!$this->value) {
+ $query->orWhereDoesntHave('status');
+ }
+ });
+ }
+}
diff --git a/app/SearchTypes/WhereInIds.php b/app/Criteria/WhereInIds.php
similarity index 64%
rename from app/SearchTypes/WhereInIds.php
rename to app/Criteria/WhereInIds.php
index 7717aed02..aee963274 100644
--- a/app/SearchTypes/WhereInIds.php
+++ b/app/Criteria/WhereInIds.php
@@ -1,11 +1,11 @@
input('name'),
+ slug: $request->input('slug'),
+ description: $request->input('description', new Missing()),
+ type: $request->input('type'),
+ global: $request->input('global'),
+ sortable: $request->input('sortable'),
+ options: $request->has('options') ? array_map(
+ fn ($data) => AttributeOptionDto::fromDataArray($data),
+ $request->input('options')
+ ) : [],
+ );
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function getSlug(): string
+ {
+ return $this->slug;
+ }
+
+ public function getDescription(): string|null|Missing
+ {
+ return $this->description;
+ }
+
+ public function getType(): string
+ {
+ return $this->type;
+ }
+
+ public function isGlobal(): bool
+ {
+ return $this->global;
+ }
+
+ public function isSortable(): bool
+ {
+ return $this->sortable;
+ }
+
+ /**
+ * @return array
+ */
+ public function getOptions(): array
+ {
+ return $this->options;
+ }
+}
diff --git a/app/Dtos/AttributeOptionDto.php b/app/Dtos/AttributeOptionDto.php
new file mode 100644
index 000000000..2af58d3d2
--- /dev/null
+++ b/app/Dtos/AttributeOptionDto.php
@@ -0,0 +1,67 @@
+input('id', new Missing()),
+ name: $request->input('name', new Missing()),
+ value_number: $request->input('value_number', new Missing()),
+ value_date: $request->input('value_date', new Missing()),
+ );
+ }
+
+ public static function fromDataArray(array $data): self
+ {
+ return new self(
+ id: array_key_exists('id', $data) ? $data['id'] : null,
+ name: array_key_exists('name', $data) ? $data['name'] : null,
+ value_number: array_key_exists('value_number', $data) ? $data['value_number'] : null,
+ value_date: array_key_exists('value_date', $data) ? $data['value_date'] : null,
+ );
+ }
+
+ /**
+ * @return string|Missing|null
+ */
+ public function getId(): string|null|Missing
+ {
+ return $this->id;
+ }
+
+ /**
+ * @return string|Missing|null
+ */
+ public function getName(): string|null|Missing
+ {
+ return $this->name;
+ }
+
+ /**
+ * @return float|Missing|null
+ */
+ public function getValueNumber(): float|null|Missing
+ {
+ return $this->value_number;
+ }
+
+ /**
+ * @return string|Missing|null
+ */
+ public function getValueDate(): string|null|Missing
+ {
+ return $this->value_date;
+ }
+}
diff --git a/app/Dtos/MetadataDto.php b/app/Dtos/MetadataDto.php
new file mode 100644
index 000000000..c2524a12c
--- /dev/null
+++ b/app/Dtos/MetadataDto.php
@@ -0,0 +1,44 @@
+name;
+ }
+
+ public function getValue(): bool|int|float|string|null
+ {
+ return $this->value;
+ }
+
+ public function isPublic(): bool
+ {
+ return $this->public;
+ }
+
+ public function getValueType(): string
+ {
+ return $this->value_type;
+ }
+}
diff --git a/app/Dtos/ProductCreateDto.php b/app/Dtos/ProductCreateDto.php
new file mode 100644
index 000000000..fcae5edb6
--- /dev/null
+++ b/app/Dtos/ProductCreateDto.php
@@ -0,0 +1,113 @@
+input('name'),
+ slug: $request->input('slug'),
+ price: $request->input('price'),
+ public: $request->boolean('public'),
+ order: $request->input('order', new Missing()),
+ quantity_step: $request->input('quantity_step', new Missing()),
+ description_html: $request->input('description_html'),
+ description_short: $request->input('description_short'),
+ media: $request->input('media', new Missing()),
+ tags: $request->input('tags', new Missing()),
+ schemas: $request->input('schemas', new Missing()),
+ sets: $request->input('sets', new Missing()),
+ items: $request->input('items', new Missing()),
+ seo: SeoMetadataDto::fromFormRequest($request),
+ metadata: self::mapMetadata($request),
+ attributes: $request->input('attributes', new Missing()),
+ );
+ }
+
+ public static function mapMetadata(Request $request): array|Missing
+ {
+ $metadata = Collection::make();
+
+ if ($request->has('metadata')) {
+ foreach ($request->input('metadata') as $key => $value) {
+ $metadata->push(MetadataDto::manualInit($key, $value, true));
+ }
+ }
+
+ if ($request->has('metadata_private')) {
+ foreach ($request->input('metadata_private') as $key => $value) {
+ $metadata->push(MetadataDto::manualInit($key, $value, false));
+ }
+ }
+
+ return $metadata->isEmpty() ? new Missing() : $metadata->toArray();
+ }
+
+ public function getMedia(): Missing|array
+ {
+ return $this->media;
+ }
+
+ public function getTags(): Missing|array
+ {
+ return $this->tags;
+ }
+
+ public function getSchemas(): Missing|array
+ {
+ return $this->schemas;
+ }
+
+ public function getSets(): Missing|array
+ {
+ return $this->sets;
+ }
+
+ public function getItems(): Missing|array
+ {
+ return $this->items;
+ }
+
+ public function getSeo(): SeoMetadataDto
+ {
+ return $this->seo;
+ }
+
+ public function getMetadata(): Missing|array
+ {
+ return $this->metadata;
+ }
+
+ public function getAttributes(): Missing|array
+ {
+ return $this->attributes;
+ }
+}
diff --git a/app/Dtos/ProductSearchDto.php b/app/Dtos/ProductSearchDto.php
new file mode 100644
index 000000000..90ce59b12
--- /dev/null
+++ b/app/Dtos/ProductSearchDto.php
@@ -0,0 +1,74 @@
+input('search'),
+ sort: $request->input('sort'),
+ ids: $request->input('ids', new Missing()),
+ slug: $request->input('slug', new Missing()),
+ name: $request->input('name', new Missing()),
+ public: self::boolean('public', $request),
+ available: self::boolean('available', $request),
+ sets: self::array('sets', $request),
+ tags: self::array('tags', $request),
+ metadata: self::array('metadata', $request),
+ metadata_private: self::array('metadata_private', $request),
+ );
+ }
+
+ public function getSearch(): ?string
+ {
+ return $this->search;
+ }
+
+ public function getSort(): ?string
+ {
+ return $this->sort;
+ }
+
+ private static function boolean(string $key, ProductIndexRequest $request): bool|Missing
+ {
+ if (!$request->has($key)) {
+ return new Missing();
+ }
+
+ return $request->boolean($key);
+ }
+
+ private static function array(string $key, ProductIndexRequest $request): array|Missing
+ {
+ if (!$request->has($key) || $request->input($key) === null) {
+ return new Missing();
+ }
+
+ if (!is_array($request->input($key))) {
+ return [$request->input($key)];
+ }
+
+ return $request->input($key);
+ }
+}
diff --git a/app/Dtos/ProductSetDto.php b/app/Dtos/ProductSetDto.php
index 2709d5b07..f7dbb6fc7 100644
--- a/app/Dtos/ProductSetDto.php
+++ b/app/Dtos/ProductSetDto.php
@@ -19,6 +19,7 @@ class ProductSetDto extends Dto
private SeoMetadataDto $seo;
private string|null|Missing $description_html;
private string|null|Missing $cover_id;
+ private array|null|Missing $attributes_ids;
public static function fromFormRequest(ProductSetStoreRequest|ProductSetUpdateRequest $request): self
{
@@ -33,6 +34,7 @@ public static function fromFormRequest(ProductSetStoreRequest|ProductSetUpdateRe
seo: SeoMetadataDto::fromFormRequest($request),
description_html: $request->input('description_html'),
cover_id: $request->input('cover_id'),
+ attributes_ids: $request->input('attributes'),
);
}
@@ -85,4 +87,9 @@ public function getCoverId(): Missing|string|null
{
return $this->cover_id;
}
+
+ public function getAttributesIds(): Missing|null|array
+ {
+ return $this->attributes_ids;
+ }
}
diff --git a/app/Dtos/ProductUpdateDto.php b/app/Dtos/ProductUpdateDto.php
new file mode 100644
index 000000000..24a8dc9b3
--- /dev/null
+++ b/app/Dtos/ProductUpdateDto.php
@@ -0,0 +1,91 @@
+input('name', new Missing()),
+ slug: $request->input('slug', new Missing()),
+ price: $request->input('price', new Missing()),
+ public: $request->boolean('public', new Missing()),
+ order: $request->input('order', new Missing()),
+ quantity_step: $request->input('quantity_step', new Missing()),
+ description_html: $request->input('description_html', new Missing()),
+ description_short: $request->input('description_short', new Missing()),
+ media: $request->input('media', new Missing()),
+ tags: $request->input('tags', new Missing()),
+ schemas: $request->input('schemas', new Missing()),
+ sets: $request->input('sets', new Missing()),
+ items: $request->input('items', new Missing()),
+ seo: SeoMetadataDto::fromFormRequest($request),
+ attributes: $request->input('attributes', new Missing()),
+ );
+ }
+
+ public function getMedia(): Missing|array
+ {
+ return $this->media;
+ }
+
+ public function getTags(): Missing|array
+ {
+ return $this->tags;
+ }
+
+ public function getSchemas(): Missing|array
+ {
+ return $this->schemas;
+ }
+
+ public function getSets(): Missing|array
+ {
+ return $this->sets;
+ }
+
+ public function getItems(): Missing|array
+ {
+ return $this->items;
+ }
+
+ public function getSeo(): SeoMetadataDto
+ {
+ return $this->seo;
+ }
+
+ public function getMetadata(): Missing
+ {
+ return new Missing();
+ }
+
+ public function getAttributes(): Missing|array
+ {
+ return $this->attributes;
+ }
+}
diff --git a/app/Dtos/RoleSearchDto.php b/app/Dtos/RoleSearchDto.php
index c428e6528..5e1331f4c 100644
--- a/app/Dtos/RoleSearchDto.php
+++ b/app/Dtos/RoleSearchDto.php
@@ -13,6 +13,8 @@ public function __construct(
private ?string $name,
private ?string $description,
private ?bool $assignable,
+ private ?array $metadata,
+ private ?array $metadata_private,
) {
}
@@ -36,6 +38,14 @@ public function toArray(): array
$data['assignable'] = $this->getAssignable();
}
+ if ($this->getMetadata() !== null) {
+ $data['metadata'] = $this->getMetadata();
+ }
+
+ if ($this->getMetadataPrivate() !== null) {
+ $data['metadata_private'] = $this->getMetadataPrivate();
+ }
+
return $data;
}
@@ -46,6 +56,8 @@ public static function instantiateFromRequest(Request $request): self
$request->input('name', null),
$request->input('description', null),
$request->has('assignable') ? $request->boolean('assignable') : null,
+ $request->input('metadata', null),
+ $request->input('metadata_private', null),
);
}
@@ -68,4 +80,14 @@ public function getAssignable(): ?bool
{
return $this->assignable;
}
+
+ public function getMetadata(): ?array
+ {
+ return $this->metadata;
+ }
+
+ public function getMetadataPrivate(): ?array
+ {
+ return $this->metadata_private;
+ }
}
diff --git a/app/Dtos/SeoMetadataDto.php b/app/Dtos/SeoMetadataDto.php
index e80e454c6..09fc78ef5 100644
--- a/app/Dtos/SeoMetadataDto.php
+++ b/app/Dtos/SeoMetadataDto.php
@@ -3,8 +3,7 @@
namespace App\Dtos;
use App\Enums\TwitterCardType;
-use App\Http\Requests\SeoMetadataRequest;
-use App\Http\Requests\SeoMetadataRulesRequest;
+use App\Http\Requests\Contracts\SeoRequestContract;
use Heseya\Dto\Dto;
use Heseya\Dto\Missing;
@@ -19,9 +18,10 @@ class SeoMetadataDto extends Dto
private string|null|Missing $model_type;
private bool|Missing $no_index;
- public static function fromFormRequest(SeoMetadataRulesRequest|SeoMetadataRequest $request): self
+ public static function fromFormRequest(SeoRequestContract $request): self
{
$seo = $request->has('seo') ? 'seo.' : '';
+
return new self(
title: $request->input($seo . 'title', new Missing()),
description: $request->input($seo . 'description', new Missing()),
diff --git a/app/Enums/AttributeType.php b/app/Enums/AttributeType.php
new file mode 100644
index 000000000..d08a3cf12
--- /dev/null
+++ b/app/Enums/AttributeType.php
@@ -0,0 +1,13 @@
+ self::BOOLEAN,
+ 'integer', 'double' => self::NUMBER,
+ default => self::STRING,
+ };
+ }
+}
diff --git a/app/Http/Controllers/AppController.php b/app/Http/Controllers/AppController.php
index 108a3bcd8..0a3c5ba71 100644
--- a/app/Http/Controllers/AppController.php
+++ b/app/Http/Controllers/AppController.php
@@ -4,6 +4,7 @@
use App\Dtos\AppInstallDto;
use App\Http\Requests\AppDeleteRequest;
+use App\Http\Requests\AppIndexRequest;
use App\Http\Requests\AppStoreRequest;
use App\Http\Resources\AppResource;
use App\Models\App;
@@ -19,9 +20,12 @@ public function __construct(private AppServiceContract $appService)
{
}
- public function index(): JsonResource
+ public function index(AppIndexRequest $request): JsonResource
{
- return AppResource::collection(App::paginate(Config::get('pagination.per_page')));
+ $apps = App::searchByCriteria($request->validated())
+ ->with('metadata');
+
+ return AppResource::collection($apps->paginate(Config::get('pagination.per_page')));
}
public function show(App $app): JsonResource
diff --git a/app/Http/Controllers/AttributeController.php b/app/Http/Controllers/AttributeController.php
new file mode 100644
index 000000000..ef860eb79
--- /dev/null
+++ b/app/Http/Controllers/AttributeController.php
@@ -0,0 +1,65 @@
+validated())
+ ->with('options');
+
+ return AttributeResource::collection(
+ $query->paginate(Config::get('pagination.per_page'))
+ );
+ }
+
+ public function show(Attribute $attribute): JsonResource
+ {
+ $attribute->load('options');
+
+ return AttributeResource::make($attribute);
+ }
+
+ public function store(AttributeStoreRequest $request): JsonResource
+ {
+ $attribute = $this->attributeService->create(
+ AttributeDto::fromFormRequest($request)
+ );
+
+ return AttributeResource::make($attribute);
+ }
+
+ public function update(Attribute $attribute, AttributeUpdateRequest $request): JsonResource
+ {
+ $attribute = $this->attributeService->update(
+ $attribute,
+ AttributeDto::fromFormRequest($request)
+ );
+
+ return AttributeResource::make($attribute);
+ }
+
+ public function destroy(Attribute $attribute): JsonResponse
+ {
+ $this->attributeService->delete($attribute);
+
+ return Response::json(null, JsonResponse::HTTP_NO_CONTENT);
+ }
+}
diff --git a/app/Http/Controllers/AttributeOptionController.php b/app/Http/Controllers/AttributeOptionController.php
new file mode 100644
index 000000000..a2a2ed516
--- /dev/null
+++ b/app/Http/Controllers/AttributeOptionController.php
@@ -0,0 +1,51 @@
+attributeOptionService->create(
+ $attribute->getKey(),
+ AttributeOptionDto::fromFormRequest($request)
+ );
+
+ return AttributeOptionResource::make($attributeOption);
+ }
+
+ public function update(Attribute $attribute, AttributeOption $option, AttributeOptionRequest $request): JsonResource
+ {
+ if (!$request->has('id')) {
+ $request->merge(['id' => $option->getKey()]);
+ }
+
+ $attributeOption = $this->attributeOptionService->updateOrCreate(
+ $attribute->getKey(),
+ AttributeOptionDto::fromFormRequest($request)
+ );
+
+ return AttributeOptionResource::make($attributeOption);
+ }
+
+ public function destroy(Attribute $attribute, AttributeOption $option): JsonResponse
+ {
+ $this->attributeOptionService->delete($option);
+
+ return Response::json(null, JsonResponse::HTTP_NO_CONTENT);
+ }
+}
diff --git a/app/Http/Controllers/DiscountController.php b/app/Http/Controllers/DiscountController.php
index db737544d..8280806e2 100644
--- a/app/Http/Controllers/DiscountController.php
+++ b/app/Http/Controllers/DiscountController.php
@@ -19,9 +19,9 @@ class DiscountController extends Controller
{
public function index(DiscountIndexRequest $request): JsonResource
{
- $query = Discount::search($request->validated())
+ $query = Discount::searchByCriteria($request->validated())
->orderBy('updated_at', 'DESC')
- ->with('orders');
+ ->with(['orders', 'metadata']);
return DiscountResource::collection(
$query->paginate(Config::get('pagination.per_page')),
diff --git a/app/Http/Controllers/FilterController.php b/app/Http/Controllers/FilterController.php
new file mode 100644
index 000000000..6641c4e6c
--- /dev/null
+++ b/app/Http/Controllers/FilterController.php
@@ -0,0 +1,25 @@
+has('sets')) {
+ return AttributeResource::collection(Attribute::where('global', true)->get());
+ }
+
+ return AttributeResource::collection(
+ Attribute::whereHas(
+ 'productSets',
+ fn ($query) => $query->whereIn('product_set_id', $request->input('sets')),
+ )->orWhere('global', true)->with('options')->get()
+ );
+ }
+}
diff --git a/app/Http/Controllers/ItemController.php b/app/Http/Controllers/ItemController.php
index ff92e8248..94dbc1668 100644
--- a/app/Http/Controllers/ItemController.php
+++ b/app/Http/Controllers/ItemController.php
@@ -19,9 +19,9 @@ class ItemController extends Controller
{
public function index(ItemIndexRequest $request): JsonResource
{
- $items = Item::search($request->validated())
+ $items = Item::searchByCriteria($request->validated())
->sort($request->input('sort', 'sku'))
- ->with('deposits');
+ ->with(['deposits', 'metadata']);
return ItemResource::collection(
$items->paginate(Config::get('pagination.per_page')),
diff --git a/app/Http/Controllers/MetadataController.php b/app/Http/Controllers/MetadataController.php
new file mode 100644
index 000000000..0afacb0c8
--- /dev/null
+++ b/app/Http/Controllers/MetadataController.php
@@ -0,0 +1,48 @@
+metadataService->returnModel($request->segments());
+
+ if ($model === null) {
+ return Response::json(null, JsonResponse::HTTP_UNPROCESSABLE_ENTITY);
+ }
+
+ $model = $model->findOrFail($modelId);
+ $public = Collection::make($request->segments())->last() === 'metadata';
+
+ foreach ($request->all() as $key => $value) {
+ $dto = MetadataDto::manualInit(name: $key, value: $value, public: $public);
+
+ $this->metadataService->updateOrCreate(
+ $model,
+ $dto
+ );
+ }
+
+ $model->refresh();
+
+ if ($public) {
+ return MetadataResource::make($model->metadata);
+ }
+
+ return MetadataResource::make($model->metadataPrivate);
+ }
+}
diff --git a/app/Http/Controllers/OrderController.php b/app/Http/Controllers/OrderController.php
index 9ca90fef7..2ff8733cc 100644
--- a/app/Http/Controllers/OrderController.php
+++ b/app/Http/Controllers/OrderController.php
@@ -51,9 +51,9 @@ public function index(OrderIndexRequest $request): JsonResource
$search_data = !$request->has('status_id')
? $request->validated() + ['status.hidden' => 0] : $request->validated();
- $query = Order::search($search_data)
+ $query = Order::searchByCriteria($search_data)
->sort($request->input('sort'))
- ->with(['products', 'discounts', 'payments']);
+ ->with(['products', 'discounts', 'payments', 'metadata']);
return OrderResource::collection(
$query->paginate(Config::get('pagination.per_page')),
diff --git a/app/Http/Controllers/PackageTemplateController.php b/app/Http/Controllers/PackageTemplateController.php
index fb1e0ed50..214b445b5 100644
--- a/app/Http/Controllers/PackageTemplateController.php
+++ b/app/Http/Controllers/PackageTemplateController.php
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
+use App\Http\Requests\PackageTemplateIndexRequest;
use App\Http\Resources\PackageTemplateResource;
use App\Models\PackageTemplate;
use Illuminate\Http\JsonResponse;
@@ -11,11 +12,12 @@
class PackageTemplateController extends Controller
{
- public function index(): JsonResource
+ public function index(PackageTemplateIndexRequest $request): JsonResource
{
- $packages = PackageTemplate::all();
+ $packages = PackageTemplate::searchByCriteria($request->validated())
+ ->with(['metadata']);
- return PackageTemplateResource::collection($packages);
+ return PackageTemplateResource::collection($packages->get());
}
public function store(Request $request): JsonResource
diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php
index a01c4d045..5f325e475 100644
--- a/app/Http/Controllers/PageController.php
+++ b/app/Http/Controllers/PageController.php
@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Dtos\PageDto;
+use App\Http\Requests\PageIndexRequest;
use App\Http\Requests\PageReorderRequest;
use App\Http\Requests\PageStoreRequest;
use App\Http\Requests\PageUpdateRequest;
@@ -22,10 +23,10 @@ public function __construct(PageServiceContract $pageService)
$this->pageService = $pageService;
}
- public function index(): JsonResource
+ public function index(PageIndexRequest $request): JsonResource
{
return PageResource::collection(
- $this->pageService->getPaginated(),
+ $this->pageService->getPaginated($request->validated()),
);
}
diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php
index db2a06fee..2b4a8de11 100644
--- a/app/Http/Controllers/ProductController.php
+++ b/app/Http/Controllers/ProductController.php
@@ -2,26 +2,18 @@
namespace App\Http\Controllers;
-use App\Dtos\SeoMetadataDto;
-use App\Events\ProductCreated;
-use App\Events\ProductDeleted;
-use App\Events\ProductUpdated;
+use App\Dtos\ProductCreateDto;
+use App\Dtos\ProductSearchDto;
+use App\Dtos\ProductUpdateDto;
use App\Http\Requests\ProductCreateRequest;
use App\Http\Requests\ProductIndexRequest;
-use App\Http\Requests\ProductShowRequest;
use App\Http\Requests\ProductUpdateRequest;
use App\Http\Resources\ProductResource;
use App\Models\Product;
-use App\Models\ProductSet;
-use App\Services\Contracts\MediaServiceContract;
+use App\Repositories\Contracts\ProductRepositoryContract;
use App\Services\Contracts\ProductServiceContract;
-use App\Services\Contracts\ProductSetServiceContract;
-use App\Services\Contracts\SchemaServiceContract;
-use App\Services\Contracts\SeoMetadataServiceContract;
-use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
-use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -29,68 +21,21 @@
class ProductController extends Controller
{
public function __construct(
- private MediaServiceContract $mediaService,
- private SchemaServiceContract $schemaService,
private ProductServiceContract $productService,
- private ProductSetServiceContract $productSetService,
- private SeoMetadataServiceContract $seoMetadataService
+ private ProductRepositoryContract $productRepository,
) {
}
public function index(ProductIndexRequest $request): JsonResource
{
- $query = Product::search($request->validated());
-
- $query
- ->sort($request->input('sort', 'order'))
- ->with(['media', 'tags', 'schemas', 'sets', 'seo', 'items']);
-
- if (Gate::denies('products.show_hidden')) {
- $query->public();
- }
-
- if ($request->has('sets')) {
- $setsFound = ProductSet::whereIn(
- 'slug',
- $request->input('sets') ?? [],
- )->with('allChildren')->get();
-
- $canShowHiddenSets = Gate::allows('product_sets.show_hidden');
- $relationScope = $canShowHiddenSets ? 'allChildren' : 'allChildrenPublic';
-
- $setsFlat = $this->productSetService
- ->flattenSetsTree($setsFound, $relationScope)
- ->map(fn (ProductSet $set) => $set->slug);
-
- $query->whereHas('sets', function (Builder $query) use ($setsFlat) {
- return $query->whereIn(
- 'slug',
- $setsFlat,
- );
- });
- }
-
- if (
- Gate::denies('products.show_hidden') &&
- !$request->hasAny(['sets', 'search', 'name', 'slug', 'public', 'tags'])
- ) {
- $query->whereDoesntHave('sets', function (Builder $query) {
- return $query->where('hide_on_index', true);
- });
- }
-
- $products = $query->paginate(Config::get('pagination.per_page'));
-
- if ($request->has('available')) {
- $products = $products->filter(function ($product) use ($request) {
- return $product->available === $request->boolean('available');
- });
- }
+ $products = $this->productRepository->search(
+ ProductSearchDto::instantiateFromRequest($request),
+ );
return ProductResource::collection($products)->full($request->has('full'));
}
- public function show(ProductShowRequest $request, Product $product): JsonResource
+ public function show(Product $product): JsonResource
{
if (Gate::denies('products.show_hidden') && !$product->public) {
throw new NotFoundHttpException();
@@ -101,66 +46,26 @@ public function show(ProductShowRequest $request, Product $product): JsonResourc
public function store(ProductCreateRequest $request): JsonResource
{
- $product = Product::create($request->validated());
-
- $this->productService->assignItems($product, $request->items);
-
- $this->productSetup($product, $request);
-
- $seo_dto = SeoMetadataDto::fromFormRequest($request);
- $product->seo()->save($this->seoMetadataService->create($seo_dto));
-
- ProductCreated::dispatch($product);
+ $product = $this->productService->create(
+ ProductCreateDto::instantiateFromRequest($request),
+ );
return ProductResource::make($product);
}
- public function productSetup(
- Product $product,
- ProductCreateRequest|ProductUpdateRequest $request,
- ): void {
- $this->mediaService->sync($product, $request->input('media', []));
- $product->tags()->sync($request->input('tags', []));
-
- if ($request->has('schemas')) {
- $this->schemaService->sync($product, $request->input('schemas'));
- }
-
- $this->productService->updateMinMaxPrices($product);
-
- if ($request->has('sets')) {
- $product->sets()->sync($request->input('sets'));
- }
- }
-
public function update(ProductUpdateRequest $request, Product $product): JsonResource
{
- $product->update($request->validated());
-
- $this->productService->assignItems($product, $request->items);
-
- $this->productSetup($product, $request);
-
- if ($request->has('seo')) {
- $seo_dto = SeoMetadataDto::fromFormRequest($request);
- $this->seoMetadataService->update($seo_dto, $product->seo);
- }
-
- ProductUpdated::dispatch($product);
+ $product = $this->productService->update(
+ $product,
+ ProductUpdateDto::instantiateFromRequest($request),
+ );
return ProductResource::make($product);
}
public function destroy(Product $product): JsonResponse
{
- $this->mediaService->sync($product);
-
- if ($product->delete()) {
- ProductDeleted::dispatch($product);
- if ($product->seo !== null) {
- $this->seoMetadataService->delete($product->seo);
- }
- }
+ $this->productService->delete($product);
return Response::json(null, 204);
}
diff --git a/app/Http/Controllers/SchemaController.php b/app/Http/Controllers/SchemaController.php
index 634fe1304..1251ab184 100644
--- a/app/Http/Controllers/SchemaController.php
+++ b/app/Http/Controllers/SchemaController.php
@@ -24,7 +24,7 @@ public function __construct(
public function index(IndexSchemaRequest $request): JsonResource
{
- $schemas = Schema::search($request->validated())->sort($request->input('sort'));
+ $schemas = Schema::searchByCriteria($request->validated())->sort($request->input('sort'));
return SchemaResource::collection(
$schemas->paginate(Config::get('pagination.per_page')),
diff --git a/app/Http/Controllers/SeoMetadataController.php b/app/Http/Controllers/SeoMetadataController.php
index d2299695a..4debb827f 100644
--- a/app/Http/Controllers/SeoMetadataController.php
+++ b/app/Http/Controllers/SeoMetadataController.php
@@ -5,7 +5,7 @@
use App\Dtos\SeoKeywordsDto;
use App\Dtos\SeoMetadataDto;
use App\Http\Requests\SeoKeywordsRequest;
-use App\Http\Requests\SeoMetadataRequest;
+use App\Http\Requests\SeoRequest;
use App\Http\Resources\SeoKeywordsResource;
use App\Http\Resources\SeoMetadataResource;
use App\Models\SeoMetadata;
@@ -27,7 +27,7 @@ public function show(SeoMetadata $seoMetadata): JsonResource
return SeoMetadataResource::make($this->seoMetadataService->show());
}
- public function createOrUpdate(SeoMetadataRequest $request): JsonResource
+ public function createOrUpdate(SeoRequest $request): JsonResource
{
$seo = SeoMetadataDto::fromFormRequest($request);
return SeoMetadataResource::make($this->seoMetadataService->createOrUpdate($seo));
diff --git a/app/Http/Controllers/ShippingMethodController.php b/app/Http/Controllers/ShippingMethodController.php
index cfe2767db..2a67baa57 100644
--- a/app/Http/Controllers/ShippingMethodController.php
+++ b/app/Http/Controllers/ShippingMethodController.php
@@ -24,6 +24,7 @@ public function __construct(
public function index(ShippingMethodIndexRequest $request): JsonResource
{
$shippingMethods = $this->shippingMethodService->index(
+ $request->only('metadata', 'metadata_private'),
$request->input('country'),
$request->input('cart_value', 0),
);
diff --git a/app/Http/Controllers/StatusController.php b/app/Http/Controllers/StatusController.php
index e46c9ca50..6a4026d61 100644
--- a/app/Http/Controllers/StatusController.php
+++ b/app/Http/Controllers/StatusController.php
@@ -4,6 +4,7 @@
use App\Exceptions\Error;
use App\Http\Requests\StatusCreateRequest;
+use App\Http\Requests\StatusIndexRequest;
use App\Http\Requests\StatusReorderRequest;
use App\Http\Requests\StatusUpdateRequest;
use App\Http\Resources\StatusResource;
@@ -14,9 +15,14 @@
class StatusController extends Controller
{
- public function index(): JsonResource
+ public function index(StatusIndexRequest $request): JsonResource
{
- return StatusResource::collection(Status::orderBy('order')->get());
+ $statuses = Status::searchByCriteria($request->validated())
+ ->with(['metadata']);
+
+ return StatusResource::collection(
+ $statuses->orderBy('order')->get()
+ );
}
public function store(StatusCreateRequest $request): JsonResource
diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php
index bdcf2eaa0..96bf7ec1e 100644
--- a/app/Http/Controllers/TagController.php
+++ b/app/Http/Controllers/TagController.php
@@ -15,7 +15,7 @@ class TagController extends Controller
{
public function index(TagIndexRequest $request): JsonResource
{
- $tags = Tag::search($request->validated())
+ $tags = Tag::searchByCriteria($request->validated())
->withCount('products')
->orderBy('products_count', 'DESC')
->paginate(Config::get('pagination.per_page'));
diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index c2467ed27..a29c1b786 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -24,7 +24,7 @@ public function __construct(UserServiceContract $userService)
public function index(UserIndexRequest $request): JsonResource
{
$paginator = $this->userService->index(
- $request->only('name', 'email', 'search', 'ids'),
+ $request->only('name', 'email', 'search', 'ids', 'metadata', 'metadata_private'),
$request->input('sort', 'created_at:asc'),
$request->input('pagination_limit', 12)
);
diff --git a/app/Http/Requests/AppIndexRequest.php b/app/Http/Requests/AppIndexRequest.php
new file mode 100644
index 000000000..e95660bfe
--- /dev/null
+++ b/app/Http/Requests/AppIndexRequest.php
@@ -0,0 +1,16 @@
+ ['nullable', 'array'],
+ 'metadata_private' => ['nullable', 'array'],
+ ];
+ }
+}
diff --git a/app/Http/Requests/AttributeIndexRequest.php b/app/Http/Requests/AttributeIndexRequest.php
new file mode 100644
index 000000000..f8055e4ee
--- /dev/null
+++ b/app/Http/Requests/AttributeIndexRequest.php
@@ -0,0 +1,20 @@
+ ['nullable', 'boolean'],
+ ];
+ }
+}
diff --git a/app/Http/Requests/AttributeOptionRequest.php b/app/Http/Requests/AttributeOptionRequest.php
new file mode 100644
index 000000000..22122ecf3
--- /dev/null
+++ b/app/Http/Requests/AttributeOptionRequest.php
@@ -0,0 +1,28 @@
+attribute?->type->value ?? $this->input('type')) {
+ AttributeType::SINGLE_OPTION => 'required',
+ default => 'nullable'
+ };
+
+ return [
+ 'name' => [$nameRule, 'string', 'max:255'],
+ 'value_number' => ['nullable', 'numeric', 'regex:/^\d{1,6}(\.\d{1,2}|)$/'],
+ 'value_date' => ['nullable', 'date'],
+ ];
+ }
+}
diff --git a/app/Http/Requests/AttributeStoreRequest.php b/app/Http/Requests/AttributeStoreRequest.php
new file mode 100644
index 000000000..5b5672a79
--- /dev/null
+++ b/app/Http/Requests/AttributeStoreRequest.php
@@ -0,0 +1,35 @@
+all());
+ $optionRules = [];
+
+ foreach ($attributeOptionRequest->rules() as $field => $rules) {
+ $optionRules['options.*.' . $field] = $rules;
+ }
+
+ return array_merge([
+ 'name' => ['required', 'string', 'max:255'],
+ 'slug' => ['required', 'string', 'max:255', 'unique:attributes'],
+ 'description' => ['nullable', 'string', 'max:255'],
+ 'type' => ['required', new EnumValue(AttributeType::class, false)],
+ 'global' => ['required', 'boolean'],
+ 'sortable' => ['required', 'boolean'],
+ 'options' => ['nullable', 'array'],
+ ], $optionRules);
+ }
+}
diff --git a/app/Http/Requests/AttributeUpdateRequest.php b/app/Http/Requests/AttributeUpdateRequest.php
new file mode 100644
index 000000000..1f50192f1
--- /dev/null
+++ b/app/Http/Requests/AttributeUpdateRequest.php
@@ -0,0 +1,32 @@
+ignore($this->attribute, 'slug'),
+ ];
+
+ $rules['type'] = [
+ 'required',
+ Rule::in($this->attribute->type),
+ ];
+
+ return $rules;
+ }
+}
diff --git a/app/Http/Requests/Contracts/MetadataRequestContract.php b/app/Http/Requests/Contracts/MetadataRequestContract.php
new file mode 100644
index 000000000..39724be4b
--- /dev/null
+++ b/app/Http/Requests/Contracts/MetadataRequestContract.php
@@ -0,0 +1,8 @@
+ ['string', 'max:255'],
'description' => ['string', 'max:255'],
'code' => ['string', 'max:64'],
+ 'metadata' => ['nullable', 'array'],
+ 'metadata_private' => ['nullable', 'array'],
];
}
}
diff --git a/app/Http/Requests/FilterIndexRequest.php b/app/Http/Requests/FilterIndexRequest.php
new file mode 100644
index 000000000..02bbd0cbc
--- /dev/null
+++ b/app/Http/Requests/FilterIndexRequest.php
@@ -0,0 +1,15 @@
+ ['array'],
+ ];
+ }
+}
diff --git a/app/Http/Requests/IndexSchemaRequest.php b/app/Http/Requests/IndexSchemaRequest.php
index fd20a7092..efa6e001f 100644
--- a/app/Http/Requests/IndexSchemaRequest.php
+++ b/app/Http/Requests/IndexSchemaRequest.php
@@ -15,6 +15,8 @@ public function rules(): array
'search' => ['nullable', 'string', 'max:255'],
'sort' => ['nullable', 'string', 'max:255'],
+ 'metadata' => ['nullable', 'array'],
+ 'metadata_private' => ['nullable', 'array'],
];
}
}
diff --git a/app/Http/Requests/ItemIndexRequest.php b/app/Http/Requests/ItemIndexRequest.php
index 86ec570ca..d577f2b5f 100644
--- a/app/Http/Requests/ItemIndexRequest.php
+++ b/app/Http/Requests/ItemIndexRequest.php
@@ -17,6 +17,8 @@ public function rules(): array
'sort' => ['nullable', 'string', 'max:255'],
'sold_out' => ['nullable', 'boolean', 'prohibited_unless:day,null'],
'day' => ['nullable', 'date', 'before_or_equal:now'],
+ 'metadata' => ['nullable', 'array'],
+ 'metadata_private' => ['nullable', 'array'],
];
}
diff --git a/app/Http/Requests/OrderIndexRequest.php b/app/Http/Requests/OrderIndexRequest.php
index c205737ba..698d02fc2 100644
--- a/app/Http/Requests/OrderIndexRequest.php
+++ b/app/Http/Requests/OrderIndexRequest.php
@@ -17,6 +17,8 @@ public function rules(): array
'paid' => ['nullable', 'boolean'],
'from' => ['nullable', 'date'],
'to' => ['nullable', 'date', 'after_or_equal:from'],
+ 'metadata' => ['nullable', 'array'],
+ 'metadata_private' => ['nullable', 'array'],
];
}
}
diff --git a/app/Http/Requests/PackageTemplateIndexRequest.php b/app/Http/Requests/PackageTemplateIndexRequest.php
new file mode 100644
index 000000000..2a2fdf790
--- /dev/null
+++ b/app/Http/Requests/PackageTemplateIndexRequest.php
@@ -0,0 +1,16 @@
+ ['nullable', 'array'],
+ 'metadata_private' => ['nullable', 'array'],
+ ];
+ }
+}
diff --git a/app/Http/Requests/PageIndexRequest.php b/app/Http/Requests/PageIndexRequest.php
new file mode 100644
index 000000000..baebd80b0
--- /dev/null
+++ b/app/Http/Requests/PageIndexRequest.php
@@ -0,0 +1,16 @@
+ ['nullable', 'array'],
+ 'metadata_private' => ['nullable', 'array'],
+ ];
+ }
+}
diff --git a/app/Http/Requests/PageStoreRequest.php b/app/Http/Requests/PageStoreRequest.php
index 79ed6576d..22a877218 100644
--- a/app/Http/Requests/PageStoreRequest.php
+++ b/app/Http/Requests/PageStoreRequest.php
@@ -2,15 +2,24 @@
namespace App\Http\Requests;
-class PageStoreRequest extends SeoMetadataRulesRequest
+use App\Http\Requests\Contracts\SeoRequestContract;
+use App\Traits\SeoRules;
+use Illuminate\Foundation\Http\FormRequest;
+
+class PageStoreRequest extends FormRequest implements SeoRequestContract
{
+ use SeoRules;
+
public function rules(): array
{
- return $this->rulesWithSeo([
- 'name' => ['required', 'string', 'max:255'],
- 'slug' => ['required', 'string', 'max:255'],
- 'public' => ['boolean'],
- 'content_html' => ['required', 'string', 'min:1'],
- ]);
+ return array_merge(
+ $this->seoRules('seo.'),
+ [
+ 'name' => ['required', 'string', 'max:255'],
+ 'slug' => ['required', 'string', 'max:255'],
+ 'public' => ['boolean'],
+ 'content_html' => ['required', 'string', 'min:1'],
+ ],
+ );
}
}
diff --git a/app/Http/Requests/PageUpdateRequest.php b/app/Http/Requests/PageUpdateRequest.php
index b849c8bd6..b0e87c97a 100644
--- a/app/Http/Requests/PageUpdateRequest.php
+++ b/app/Http/Requests/PageUpdateRequest.php
@@ -2,21 +2,29 @@
namespace App\Http\Requests;
+use App\Http\Requests\Contracts\SeoRequestContract;
+use App\Traits\SeoRules;
+use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
-class PageUpdateRequest extends SeoMetadataRulesRequest
+class PageUpdateRequest extends FormRequest implements SeoRequestContract
{
+ use SeoRules;
+
public function rules(): array
{
- return $this->rulesWithSeo([
- 'name' => ['string', 'max:255'],
- 'slug' => [
- 'string',
- 'max:255',
- Rule::unique('pages')->ignore($this->route('page')->slug, 'slug'),
+ return array_merge(
+ $this->seoRules(),
+ [
+ 'name' => ['string', 'max:255'],
+ 'slug' => [
+ 'string',
+ 'max:255',
+ Rule::unique('pages')->ignore($this->route('page')->slug, 'slug'),
+ ],
+ 'public' => ['boolean'],
+ 'content_html' => ['string', 'min:1'],
],
- 'public' => ['boolean'],
- 'content_html' => ['string', 'min:1'],
- ]);
+ );
}
}
diff --git a/app/Http/Requests/ProductCreateRequest.php b/app/Http/Requests/ProductCreateRequest.php
index e7b7a0a93..dd8822fbc 100644
--- a/app/Http/Requests/ProductCreateRequest.php
+++ b/app/Http/Requests/ProductCreateRequest.php
@@ -2,37 +2,56 @@
namespace App\Http\Requests;
+use App\Http\Requests\Contracts\MetadataRequestContract;
+use App\Http\Requests\Contracts\SeoRequestContract;
+use App\Rules\AttributeOptionExist;
+use App\Rules\ProductAttributeOptions;
use App\Rules\UniqueIdInRequest;
+use App\Traits\MetadataRules;
+use App\Traits\SeoRules;
+use Illuminate\Foundation\Http\FormRequest;
-class ProductCreateRequest extends SeoMetadataRulesRequest
+class ProductCreateRequest extends FormRequest implements SeoRequestContract, MetadataRequestContract
{
+ use SeoRules, MetadataRules;
+
public function rules(): array
{
- return $this->rulesWithSeo([
- 'name' => ['required', 'string', 'max:255'],
- 'slug' => ['required', 'string', 'max:255', 'unique:products', 'alpha_dash'],
- 'price' => ['required', 'numeric', 'min:0'],
- 'description_html' => ['nullable', 'string'],
- 'description_short' => ['nullable', 'string', 'between:30,5000'],
- 'public' => ['required', 'boolean'],
- 'quantity_step' => ['numeric'],
- 'order' => ['nullable', 'numeric'],
-
- 'media' => ['nullable', 'array'],
- 'media.*' => ['uuid', 'exists:media,id'],
-
- 'tags' => ['nullable', 'array'],
- 'tags.*' => ['uuid', 'exists:tags,id'],
-
- 'schemas' => ['nullable', 'array'],
- 'schemas.*' => ['uuid', 'exists:schemas,id'],
-
- 'sets' => ['nullable', 'array'],
- 'sets.*' => ['uuid', 'exists:product_sets,id'],
-
- 'items' => ['nullable', 'array', new UniqueIdInRequest()],
- 'items.*.id' => ['uuid'],
- 'items.*.quantity' => ['numeric'],
- ]);
+ return array_merge(
+ $this->seoRules(),
+ $this->metadataRules(),
+ [
+ 'name' => ['required', 'string', 'max:255'],
+ 'slug' => ['required', 'string', 'max:255', 'unique:products', 'alpha_dash'],
+ 'price' => ['required', 'numeric', 'min:0'],
+ 'public' => ['required', 'boolean'],
+
+ 'description_html' => ['nullable', 'string'],
+ 'description_short' => ['nullable', 'string', 'between:30,5000'],
+
+ 'quantity_step' => ['numeric'],
+ 'order' => ['nullable', 'numeric'],
+
+ 'media' => ['nullable', 'array'],
+ 'media.*' => ['uuid', 'exists:media,id'],
+
+ 'tags' => ['nullable', 'array'],
+ 'tags.*' => ['uuid', 'exists:tags,id'],
+
+ 'attributes' => ['nullable', 'array'],
+ 'attributes.*' => ['bail', 'array', new ProductAttributeOptions()],
+ 'attributes.*.*' => ['uuid', new AttributeOptionExist()],
+
+ 'items' => ['nullable', 'array', new UniqueIdInRequest()],
+ 'items.*.id' => ['uuid'],
+ 'items.*.quantity' => ['numeric'],
+
+ 'schemas' => ['nullable', 'array'],
+ 'schemas.*' => ['uuid', 'exists:schemas,id'],
+
+ 'sets' => ['nullable', 'array'],
+ 'sets.*' => ['uuid', 'exists:product_sets,id'],
+ ],
+ );
}
}
diff --git a/app/Http/Requests/ProductIndexRequest.php b/app/Http/Requests/ProductIndexRequest.php
index 30c8f5d57..184ab2cc8 100644
--- a/app/Http/Requests/ProductIndexRequest.php
+++ b/app/Http/Requests/ProductIndexRequest.php
@@ -2,6 +2,7 @@
namespace App\Http\Requests;
+use App\Rules\CanShowPrivateMetadata;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
@@ -19,6 +20,8 @@ public function rules(): array
}
return [
+ 'search' => ['nullable', 'string', 'max:255'],
+
'ids' => ['string'],
'name' => ['nullable', 'string', 'max:255'],
'slug' => ['nullable', 'string', 'max:255'],
@@ -28,14 +31,14 @@ public function rules(): array
'string',
$setsExist,
],
- 'search' => ['nullable', 'string', 'max:255'],
'sort' => ['nullable', 'string', 'max:255'],
'available' => ['nullable'],
'tags' => ['nullable', 'array'],
- 'tags.*' => [
- 'string',
- 'uuid',
- ],
+ 'tags.*' => ['string', 'uuid'],
+ 'metadata' => ['nullable', 'array'],
+ 'metadata_private' => ['nullable', 'array', new CanShowPrivateMetadata()],
+
+ 'full' => ['nullable', 'boolean'],
];
}
}
diff --git a/app/Http/Requests/ProductSetIndexRequest.php b/app/Http/Requests/ProductSetIndexRequest.php
index 4cb6f2dca..c971ee9ed 100644
--- a/app/Http/Requests/ProductSetIndexRequest.php
+++ b/app/Http/Requests/ProductSetIndexRequest.php
@@ -15,6 +15,8 @@ public function rules(): array
'public' => ['nullable', 'boolean'],
'tree' => ['nullable', 'boolean'],
'root' => ['nullable', 'boolean'],
+ 'metadata' => ['nullable', 'array'],
+ 'metadata_private' => ['nullable', 'array'],
];
}
}
diff --git a/app/Http/Requests/ProductSetStoreRequest.php b/app/Http/Requests/ProductSetStoreRequest.php
index c83e75af0..1615efcc1 100644
--- a/app/Http/Requests/ProductSetStoreRequest.php
+++ b/app/Http/Requests/ProductSetStoreRequest.php
@@ -2,26 +2,37 @@
namespace App\Http\Requests;
-class ProductSetStoreRequest extends SeoMetadataRulesRequest
+use App\Http\Requests\Contracts\SeoRequestContract;
+use App\Traits\SeoRules;
+use Illuminate\Foundation\Http\FormRequest;
+
+class ProductSetStoreRequest extends FormRequest implements SeoRequestContract
{
+ use SeoRules;
+
public function rules(): array
{
- return $this->rulesWithSeo([
- 'name' => ['required', 'string', 'max:255'],
- 'slug_suffix' => [
- 'required',
- 'string',
- 'max:255',
- 'alpha_dash',
+ return array_merge(
+ $this->seoRules(),
+ [
+ 'name' => ['required', 'string', 'max:255'],
+ 'slug_suffix' => [
+ 'required',
+ 'string',
+ 'max:255',
+ 'alpha_dash',
+ ],
+ 'slug_override' => ['required', 'boolean'],
+ 'public' => ['boolean'],
+ 'hide_on_index' => ['boolean'],
+ 'parent_id' => ['uuid', 'nullable', 'exists:product_sets,id'],
+ 'children_ids' => ['array'],
+ 'children_ids.*' => ['uuid', 'exists:product_sets,id'],
+ 'description_html' => ['nullable', 'string'],
+ 'cover_id' => ['uuid', 'uuid', 'exists:media,id'],
+ 'attributes' => ['array'],
+ 'attributes.*' => ['uuid', 'exists:attributes,id'],
],
- 'slug_override' => ['required', 'boolean'],
- 'public' => ['boolean'],
- 'hide_on_index' => ['boolean'],
- 'parent_id' => ['uuid', 'nullable', 'exists:product_sets,id'],
- 'children_ids' => ['array'],
- 'children_ids.*' => ['uuid', 'exists:product_sets,id'],
- 'description_html' => ['nullable', 'string'],
- 'cover_id' => ['uuid', 'uuid', 'exists:media,id'],
- ]);
+ );
}
}
diff --git a/app/Http/Requests/ProductSetUpdateRequest.php b/app/Http/Requests/ProductSetUpdateRequest.php
index 7d9a13bbb..c80ee692c 100644
--- a/app/Http/Requests/ProductSetUpdateRequest.php
+++ b/app/Http/Requests/ProductSetUpdateRequest.php
@@ -2,26 +2,37 @@
namespace App\Http\Requests;
-class ProductSetUpdateRequest extends SeoMetadataRulesRequest
+use App\Http\Requests\Contracts\SeoRequestContract;
+use App\Traits\SeoRules;
+use Illuminate\Foundation\Http\FormRequest;
+
+class ProductSetUpdateRequest extends FormRequest implements SeoRequestContract
{
+ use SeoRules;
+
public function rules(): array
{
- return $this->rulesWithSeo([
- 'name' => ['required', 'string', 'max:255'],
- 'slug_suffix' => [
- 'required',
- 'string',
- 'max:255',
- 'alpha_dash',
+ return array_merge(
+ $this->seoRules(),
+ [
+ 'name' => ['required', 'string', 'max:255'],
+ 'slug_suffix' => [
+ 'required',
+ 'string',
+ 'max:255',
+ 'alpha_dash',
+ ],
+ 'slug_override' => ['required', 'boolean'],
+ 'public' => ['required', 'boolean'],
+ 'hide_on_index' => ['required', 'boolean'],
+ 'parent_id' => ['present', 'nullable', 'uuid', 'exists:product_sets,id'],
+ 'children_ids' => ['present', 'array'],
+ 'children_ids.*' => ['uuid', 'exists:product_sets,id'],
+ 'description_html' => ['nullable', 'string'],
+ 'cover_id' => ['uuid', 'uuid', 'exists:media,id'],
+ 'attributes' => ['array'],
+ 'attributes.*' => ['uuid', 'exists:attributes,id'],
],
- 'slug_override' => ['required', 'boolean'],
- 'public' => ['required', 'boolean'],
- 'hide_on_index' => ['required', 'boolean'],
- 'parent_id' => ['present', 'nullable', 'uuid', 'exists:product_sets,id'],
- 'children_ids' => ['present', 'array'],
- 'children_ids.*' => ['uuid', 'exists:product_sets,id'],
- 'description_html' => ['nullable', 'string'],
- 'cover_id' => ['uuid', 'uuid', 'exists:media,id'],
- ]);
+ );
}
}
diff --git a/app/Http/Requests/ProductUpdateRequest.php b/app/Http/Requests/ProductUpdateRequest.php
index 237a222d7..078302df7 100644
--- a/app/Http/Requests/ProductUpdateRequest.php
+++ b/app/Http/Requests/ProductUpdateRequest.php
@@ -10,8 +10,13 @@ public function rules(): array
{
$rules = parent::rules();
+ // TODO: should be uncommented in future
+// $rules['metadata'] = ['prohibited'];
+// $rules['metadata_private'] = ['prohibited'];
+ $rules['name'] = ['string', 'max:255'];
+ $rules['price'] = ['numeric', 'min:0'];
+ $rules['public'] = ['boolean'];
$rules['slug'] = [
- 'required',
'string',
'max:255',
'alpha_dash',
diff --git a/app/Http/Requests/RoleIndexRequest.php b/app/Http/Requests/RoleIndexRequest.php
index 9e63bb273..7d5d6fe38 100644
--- a/app/Http/Requests/RoleIndexRequest.php
+++ b/app/Http/Requests/RoleIndexRequest.php
@@ -13,6 +13,8 @@ public function rules(): array
'name' => ['string'],
'description' => ['string'],
'assignable' => ['boolean'],
+ 'metadata' => ['nullable', 'array'],
+ 'metadata_private' => ['nullable', 'array'],
];
}
}
diff --git a/app/Http/Requests/SeoMetadataRequest.php b/app/Http/Requests/SeoMetadataRequest.php
deleted file mode 100644
index f00e5f20f..000000000
--- a/app/Http/Requests/SeoMetadataRequest.php
+++ /dev/null
@@ -1,22 +0,0 @@
- ['nullable', 'string', 'max:255'],
- 'description' => ['nullable', 'string', 'max:1000'],
- 'keywords' => ['nullable', 'array'],
- 'og_image_id' => ['nullable', 'uuid', 'exists:media,id'],
- 'twitter_card' => ['nullable', new EnumValue(TwitterCardType::class, false)],
- 'no_index' => ['nullable', 'boolean'],
- ];
- }
-}
diff --git a/app/Http/Requests/SeoMetadataRulesRequest.php b/app/Http/Requests/SeoMetadataRulesRequest.php
deleted file mode 100644
index ec11df0e5..000000000
--- a/app/Http/Requests/SeoMetadataRulesRequest.php
+++ /dev/null
@@ -1,16 +0,0 @@
-seoRules());
- }
-}
diff --git a/app/Http/Requests/SeoRequest.php b/app/Http/Requests/SeoRequest.php
new file mode 100644
index 000000000..0a7884bc6
--- /dev/null
+++ b/app/Http/Requests/SeoRequest.php
@@ -0,0 +1,17 @@
+seoRules('');
+ }
+}
diff --git a/app/Http/Requests/ShippingMethodIndexRequest.php b/app/Http/Requests/ShippingMethodIndexRequest.php
index 827aa592c..dbf861a91 100644
--- a/app/Http/Requests/ShippingMethodIndexRequest.php
+++ b/app/Http/Requests/ShippingMethodIndexRequest.php
@@ -11,6 +11,8 @@ public function rules(): array
return [
'country' => ['string', 'size:2', 'exists:countries,code'],
'cart_value' => ['numeric'],
+ 'metadata' => ['nullable', 'array'],
+ 'metadata_private' => ['nullable', 'array'],
];
}
}
diff --git a/app/Http/Requests/StatusIndexRequest.php b/app/Http/Requests/StatusIndexRequest.php
new file mode 100644
index 000000000..5195a8c17
--- /dev/null
+++ b/app/Http/Requests/StatusIndexRequest.php
@@ -0,0 +1,16 @@
+ ['nullable', 'array'],
+ 'metadata_private' => ['nullable', 'array'],
+ ];
+ }
+}
diff --git a/app/Http/Requests/UserIndexRequest.php b/app/Http/Requests/UserIndexRequest.php
index afa563aa2..027ed32e8 100644
--- a/app/Http/Requests/UserIndexRequest.php
+++ b/app/Http/Requests/UserIndexRequest.php
@@ -14,6 +14,8 @@ public function rules(): array
'email' => ['nullable', 'string'],
'sort' => ['nullable', 'string'],
'pagination_limit' => ['nullable', 'integer', 'min:1'],
+ 'metadata' => ['nullable', 'array'],
+ 'metadata_private' => ['nullable', 'array'],
];
}
}
diff --git a/app/Http/Resources/AppResource.php b/app/Http/Resources/AppResource.php
index fe3ac1c51..714acecc6 100644
--- a/app/Http/Resources/AppResource.php
+++ b/app/Http/Resources/AppResource.php
@@ -2,13 +2,16 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
class AppResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'url' => $this->resource->url,
'microfrontend_url' => $this->resource->microfrontend_url,
@@ -18,7 +21,7 @@ public function base(Request $request): array
'description' => $this->resource->description,
'icon' => $this->resource->icon,
'author' => $this->resource->author,
- ];
+ ], $this->metadataResource('apps.show_metadata_private'));
}
public function view(Request $request): array
diff --git a/app/Http/Resources/AttributeOptionResource.php b/app/Http/Resources/AttributeOptionResource.php
new file mode 100644
index 000000000..f75db3720
--- /dev/null
+++ b/app/Http/Resources/AttributeOptionResource.php
@@ -0,0 +1,20 @@
+ $this->resource->getKey(),
+ 'name' => $this->resource->name,
+ 'index' => $this->resource->index,
+ 'value_number' => $this->resource->value_number,
+ 'value_date' => $this->resource->value_date,
+ 'attribute_id' => $this->resource->attribute_id,
+ ];
+ }
+}
diff --git a/app/Http/Resources/AttributeResource.php b/app/Http/Resources/AttributeResource.php
new file mode 100644
index 000000000..afcd8d02d
--- /dev/null
+++ b/app/Http/Resources/AttributeResource.php
@@ -0,0 +1,31 @@
+resource->type->value) {
+ AttributeType::NUMBER => [$this->resource->min_number, $this->resource->max_number],
+ AttributeType::DATE => [$this->resource->min_date, $this->resource->max_date],
+ default => [null, null],
+ };
+
+ return [
+ 'id' => $this->resource->getKey(),
+ 'name' => $this->resource->name,
+ 'slug' => $this->resource->slug,
+ 'description' => $this->resource->description,
+ 'min' => $min,
+ 'max' => $max,
+ 'type' => $this->resource->type,
+ 'global' => $this->resource->global,
+ 'sortable' => $this->resource->sortable,
+ 'options' => AttributeOptionResource::collection($this->resource->options),
+ ];
+ }
+}
diff --git a/app/Http/Resources/DiscountResource.php b/app/Http/Resources/DiscountResource.php
index d83433e76..adfc11ff0 100644
--- a/app/Http/Resources/DiscountResource.php
+++ b/app/Http/Resources/DiscountResource.php
@@ -2,10 +2,13 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
class DiscountResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
if (isset($this->resource->pivot)) {
@@ -13,7 +16,7 @@ public function base(Request $request): array
$this->resource->discount = $this->resource->pivot->discount;
}
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'code' => $this->resource->code,
'description' => $this->resource->description,
@@ -24,6 +27,6 @@ public function base(Request $request): array
'available' => $this->resource->available,
'starts_at' => $this->resource->starts_at,
'expires_at' => $this->resource->expires_at,
- ];
+ ], $this->metadataResource('discounts.show_metadata_private'));
}
}
diff --git a/app/Http/Resources/ItemResource.php b/app/Http/Resources/ItemResource.php
index c5f9a2781..ff74da010 100644
--- a/app/Http/Resources/ItemResource.php
+++ b/app/Http/Resources/ItemResource.php
@@ -2,17 +2,20 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
class ItemResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'name' => $this->resource->name,
'sku' => $this->resource->sku,
'quantity' => $this->resource->getQuantity($request->input('day')),
- ];
+ ], $this->metadataResource('items.show_metadata_private'));
}
}
diff --git a/app/Http/Resources/MediaResource.php b/app/Http/Resources/MediaResource.php
index 9338e04bd..ae2823ec8 100644
--- a/app/Http/Resources/MediaResource.php
+++ b/app/Http/Resources/MediaResource.php
@@ -2,19 +2,22 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class MediaResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'type' => Str::lower($this->resource->type->key),
'url' => $this->resource->url,
'slug' => $this->resource->slug,
'alt' => $this->resource->alt,
- ];
+ ], $this->metadataResource('media.show_metadata_private'));
}
}
diff --git a/app/Http/Resources/MetadataResource.php b/app/Http/Resources/MetadataResource.php
new file mode 100644
index 000000000..866b6983a
--- /dev/null
+++ b/app/Http/Resources/MetadataResource.php
@@ -0,0 +1,19 @@
+resource->map(function ($metadata) use (&$resource): void {
+ $resource[$metadata->name] = $metadata->value;
+ });
+
+ return $resource;
+ }
+}
diff --git a/app/Http/Resources/OptionResource.php b/app/Http/Resources/OptionResource.php
index 617158444..88722e745 100644
--- a/app/Http/Resources/OptionResource.php
+++ b/app/Http/Resources/OptionResource.php
@@ -2,19 +2,22 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
class OptionResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'name' => $this->resource->name,
'price' => $this->resource->price,
'disabled' => $this->resource->disabled,
'available' => $this->resource->available,
'items' => ItemPublicResource::collection($this->resource->items),
- ];
+ ], $this->metadataResource('options.show_metadata_private'));
}
}
diff --git a/app/Http/Resources/OrderResource.php b/app/Http/Resources/OrderResource.php
index 4d7f33e9f..3b7973076 100644
--- a/app/Http/Resources/OrderResource.php
+++ b/app/Http/Resources/OrderResource.php
@@ -3,17 +3,20 @@
namespace App\Http\Resources;
use App\Models\User;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
class OrderResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'code' => $this->resource->code,
- 'email' => $this->resource->email,
'currency' => $this->resource->currency,
+ 'email' => $this->resource->email,
'summary' => $this->resource->summary,
'summary_paid' => $this->resource->paid_amount,
'shipping_price' => $this->resource->shipping_price,
@@ -25,7 +28,7 @@ public function base(Request $request): array
AddressResource::make($this->resource->deliveryAddress) : null,
'shipping_method' => $this->resource->shippingMethod ?
ShippingMethodResource::make($this->resource->shippingMethod) : null,
- ];
+ ], $this->metadataResource('orders.show_metadata_private'));
}
public function view(Request $request): array
diff --git a/app/Http/Resources/PackageTemplateResource.php b/app/Http/Resources/PackageTemplateResource.php
index 2a22df344..2a68ce800 100644
--- a/app/Http/Resources/PackageTemplateResource.php
+++ b/app/Http/Resources/PackageTemplateResource.php
@@ -2,19 +2,22 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
class PackageTemplateResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'name' => $this->resource->name,
'weight' => $this->resource->weight,
'width' => $this->resource->width,
'height' => $this->resource->height,
'depth' => $this->resource->depth,
- ];
+ ], $this->metadataResource('packages.show_metadata_private'));
}
}
diff --git a/app/Http/Resources/PageResource.php b/app/Http/Resources/PageResource.php
index 528ec868f..647e71e53 100644
--- a/app/Http/Resources/PageResource.php
+++ b/app/Http/Resources/PageResource.php
@@ -2,19 +2,22 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
class PageResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'slug' => $this->resource->slug,
'name' => $this->resource->name,
'public' => $this->resource->public,
'order' => $this->resource->order,
- ];
+ ], $this->metadataResource('pages.show_metadata_private'));
}
public function view(Request $request): array
diff --git a/app/Http/Resources/ProductAttributeResource.php b/app/Http/Resources/ProductAttributeResource.php
new file mode 100644
index 000000000..759cdf110
--- /dev/null
+++ b/app/Http/Resources/ProductAttributeResource.php
@@ -0,0 +1,23 @@
+ $this->resource->getKey(),
+ 'slug' => $this->resource->slug,
+ 'description' => $this->resource->description,
+ 'type' => $this->resource->type->value,
+ 'global' => $this->resource->global,
+ 'sortable' => $this->resource->sortable,
+ ]
+ );
+ }
+}
diff --git a/app/Http/Resources/ProductAttributeShortResource.php b/app/Http/Resources/ProductAttributeShortResource.php
new file mode 100644
index 000000000..077af796d
--- /dev/null
+++ b/app/Http/Resources/ProductAttributeShortResource.php
@@ -0,0 +1,16 @@
+ $this->resource->name,
+ 'selected_options' => AttributeOptionResource::collection($this->resource->pivot->options),
+ ];
+ }
+}
diff --git a/app/Http/Resources/ProductResource.php b/app/Http/Resources/ProductResource.php
index 7c4ef879e..0e65707b6 100644
--- a/app/Http/Resources/ProductResource.php
+++ b/app/Http/Resources/ProductResource.php
@@ -2,14 +2,17 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ProductResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'slug' => $this->resource->slug,
'name' => $this->resource->name,
@@ -23,7 +26,7 @@ public function base(Request $request): array
'cover' => MediaResource::make($this->resource->media->first()),
'tags' => TagResource::collection($this->resource->tags),
'items' => ProductItemResource::collection($this->resource->items),
- ];
+ ], $this->metadataResource('products.show_metadata_private'));
}
public function view(Request $request): array
@@ -45,6 +48,14 @@ public function view(Request $request): array
'schemas' => SchemaResource::collection($this->resource->schemas),
'sets' => ProductSetResource::collection($sets),
'seo' => SeoMetadataResource::make($this->resource->seo),
+ 'attributes' => ProductAttributeResource::collection($this->resource->attributes),
+ ];
+ }
+
+ public function index(Request $request): array
+ {
+ return [
+ 'attributes' => ProductAttributeShortResource::collection($this->resource->attributes),
];
}
}
diff --git a/app/Http/Resources/ProductSetChildrenResource.php b/app/Http/Resources/ProductSetChildrenResource.php
index fc703ab64..616dc08fd 100644
--- a/app/Http/Resources/ProductSetChildrenResource.php
+++ b/app/Http/Resources/ProductSetChildrenResource.php
@@ -2,18 +2,21 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class ProductSetChildrenResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
$children = Gate::denies('product_sets.show_hidden')
? $this->resource->childrenPublic
: $this->resource->children;
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'name' => $this->resource->name,
'slug' => $this->resource->slug,
@@ -25,6 +28,7 @@ public function base(Request $request): array
'parent_id' => $this->resource->parent_id,
'children' => ProductSetChildrenResource::collection($children),
'cover' => MediaResource::make($this->resource->media),
- ];
+ 'attributes' => AttributeResource::collection($this->resource->attributes),
+ ], $this->metadataResource('product_sets.show_metadata_private'));
}
}
diff --git a/app/Http/Resources/ProductSetParentChildrenResource.php b/app/Http/Resources/ProductSetParentChildrenResource.php
index 7057e4558..b3772162f 100644
--- a/app/Http/Resources/ProductSetParentChildrenResource.php
+++ b/app/Http/Resources/ProductSetParentChildrenResource.php
@@ -2,21 +2,24 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class ProductSetParentChildrenResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
$children = Gate::denies('product_sets.show_hidden')
? $this->resource->childrenPublic
: $this->resource->children;
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
- 'name' => $this->resource->name,
'slug' => $this->resource->slug,
+ 'name' => $this->resource->name,
'slug_suffix' => $this->resource->slugSuffix,
'slug_override' => $this->resource->slugOverride,
'public' => $this->resource->public,
@@ -27,6 +30,6 @@ public function base(Request $request): array
'seo' => SeoMetadataResource::make($this->resource->seo),
'description_html' => $this->resource->description_html,
'cover' => MediaResource::make($this->resource->media),
- ];
+ ], $this->metadataResource('product_sets.show_metadata_private'));
}
}
diff --git a/app/Http/Resources/ProductSetParentResource.php b/app/Http/Resources/ProductSetParentResource.php
index f61bfea48..7698aa62d 100644
--- a/app/Http/Resources/ProductSetParentResource.php
+++ b/app/Http/Resources/ProductSetParentResource.php
@@ -2,18 +2,21 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class ProductSetParentResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
$children = Gate::denies('product_sets.show_hidden')
? $this->resource->childrenPublic
: $this->resource->children;
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'name' => $this->resource->name,
'slug' => $this->resource->slug,
@@ -23,12 +26,11 @@ public function base(Request $request): array
'visible' => $this->resource->public_parent && $this->resource->public,
'hide_on_index' => $this->resource->hide_on_index,
'parent' => ProductSetResource::make($this->resource->parent),
- 'children_ids' => $children->map(
- fn ($child) => $child->getKey(),
- )->toArray(),
+ 'children_ids' => $children->map(fn ($child) => $child->getKey()),
'seo' => SeoMetadataResource::make($this->resource->seo),
'description_html' => $this->resource->description_html,
'cover' => MediaResource::make($this->resource->media),
- ];
+ 'attributes' => AttributeResource::collection($this->resource->attributes),
+ ], $this->metadataResource('product_sets.show_metadata_private'));
}
}
diff --git a/app/Http/Resources/ProductSetResource.php b/app/Http/Resources/ProductSetResource.php
index a3e6b4345..761d042fb 100644
--- a/app/Http/Resources/ProductSetResource.php
+++ b/app/Http/Resources/ProductSetResource.php
@@ -2,18 +2,21 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class ProductSetResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
$children = Gate::denies('product_sets.show_hidden')
? $this->resource->childrenPublic
: $this->resource->children;
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'name' => $this->resource->name,
'slug' => $this->resource->slug,
@@ -23,10 +26,8 @@ public function base(Request $request): array
'visible' => $this->resource->public_parent && $this->resource->public,
'hide_on_index' => $this->resource->hide_on_index,
'parent_id' => $this->resource->parent_id,
- 'children_ids' => $children->map(
- fn ($child) => $child->getKey(),
- )->toArray(),
+ 'children_ids' => $children->map(fn ($child) => $child->getKey())->toArray(),
'cover' => MediaResource::make($this->resource->media),
- ];
+ ], $this->metadataResource('product_sets.show_metadata_private'));
}
}
diff --git a/app/Http/Resources/ProductSetResourceUniversal.php b/app/Http/Resources/ProductSetResourceUniversal.php
index 7aa1aaaa4..3f9f83b36 100644
--- a/app/Http/Resources/ProductSetResourceUniversal.php
+++ b/app/Http/Resources/ProductSetResourceUniversal.php
@@ -45,7 +45,7 @@ public function base(Request $request): array
$childrenResource = $this->showChildren ? [
'children' => ProductSetResourceUniversal::collection($children)->setIsPublic($this->public),
] : [
- 'children_ids' => $children->map(fn ($child) => $child->getKey())->toArray(),
+ 'children_ids' => $children->map(fn ($child) => $child->getKey()),
];
return [
diff --git a/app/Http/Resources/RoleResource.php b/app/Http/Resources/RoleResource.php
index cd87dee69..ffdc3ad9e 100644
--- a/app/Http/Resources/RoleResource.php
+++ b/app/Http/Resources/RoleResource.php
@@ -3,14 +3,17 @@
namespace App\Http\Resources;
use App\Enums\RoleType;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RoleResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'name' => $this->resource->name,
'description' => $this->resource->description,
@@ -21,7 +24,7 @@ public function base(Request $request): array
$this->resource->getAllPermissions(),
) : false,
'deletable' => $this->resource->type->is(RoleType::REGULAR),
- ];
+ ], $this->metadataResource('roles.show_metadata_private'));
}
public function view(Request $request): array
diff --git a/app/Http/Resources/SchemaResource.php b/app/Http/Resources/SchemaResource.php
index ed00b9567..e9b92420b 100644
--- a/app/Http/Resources/SchemaResource.php
+++ b/app/Http/Resources/SchemaResource.php
@@ -2,14 +2,17 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class SchemaResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'type' => Str::lower($this->resource->type->key),
'name' => $this->resource->name,
@@ -26,7 +29,7 @@ public function base(Request $request): array
'validation' => $this->resource->validation,
'options' => OptionResource::collection($this->resource->options),
'used_schemas' => $this->resource->usedSchemas->map(fn ($schema) => $schema->getKey()),
- ];
+ ], $this->metadataResource('schemas.show_metadata_private'));
}
public function view(Request $request): array
diff --git a/app/Http/Resources/ShippingMethodResource.php b/app/Http/Resources/ShippingMethodResource.php
index c409ca36d..26f63af27 100644
--- a/app/Http/Resources/ShippingMethodResource.php
+++ b/app/Http/Resources/ShippingMethodResource.php
@@ -2,13 +2,16 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
class ShippingMethodResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'name' => $this->resource->name,
'price' => $this->resource->price,
@@ -19,6 +22,6 @@ public function base(Request $request): array
'price_ranges' => PriceRangeResource::collection($this->resource->priceRanges->sortBy('start')),
'shipping_time_min' => $this->resource->shipping_time_min,
'shipping_time_max' => $this->resource->shipping_time_max,
- ];
+ ], $this->metadataResource('shipping_methods.show_metadata_private'));
}
}
diff --git a/app/Http/Resources/StatusResource.php b/app/Http/Resources/StatusResource.php
index 33429050a..6d7eb9eb3 100644
--- a/app/Http/Resources/StatusResource.php
+++ b/app/Http/Resources/StatusResource.php
@@ -2,13 +2,16 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
class StatusResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'name' => $this->resource->name,
'color' => $this->resource->color,
@@ -16,6 +19,6 @@ public function base(Request $request): array
'description' => $this->resource->description,
'hidden' => $this->resource->hidden,
'no_notifications' => $this->resource->no_notifications,
- ];
+ ], $this->metadataResource('statuses.show_metadata_private'));
}
}
diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php
index 310cbb8fe..a2018600f 100644
--- a/app/Http/Resources/UserResource.php
+++ b/app/Http/Resources/UserResource.php
@@ -2,20 +2,23 @@
namespace App\Http\Resources;
+use App\Traits\MetadataResource;
use Illuminate\Http\Request;
class UserResource extends Resource
{
+ use MetadataResource;
+
public function base(Request $request): array
{
- return [
+ return array_merge([
'id' => $this->resource->getKey(),
'email' => $this->resource->email,
'name' => $this->resource->name,
'avatar' => $this->resource->avatar,
'roles' => RoleResource::collection($this->resource->roles),
'is_tfa_active' => $this->resource->is_tfa_active,
- ];
+ ], $this->metadataResource('users.show_metadata_private'));
}
public function view(Request $request): array
diff --git a/app/Models/App.php b/app/Models/App.php
index 8d419cef2..fea4b0f5c 100644
--- a/app/Models/App.php
+++ b/app/Models/App.php
@@ -2,8 +2,12 @@
namespace App\Models;
+use App\Criteria\MetadataPrivateSearch;
+use App\Criteria\MetadataSearch;
use App\Services\Contracts\UrlServiceContract;
+use App\Traits\HasMetadata;
use App\Traits\HasWebHooks;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
@@ -25,10 +29,12 @@ class App extends Model implements
JWTSubject
{
use HasFactory,
+ HasCriteria,
Authorizable,
Authenticatable,
HasPermissions,
- HasWebHooks;
+ HasWebHooks,
+ HasMetadata;
protected $guard_name = 'api';
@@ -47,6 +53,11 @@ class App extends Model implements
'role_id',
];
+ protected array $criteria = [
+ 'metadata' => MetadataSearch::class,
+ 'metadata_private' => MetadataPrivateSearch::class,
+ ];
+
public function setUrlAttribute(string $url): void
{
/** @var UrlServiceContract $urlService */
diff --git a/app/Models/Attribute.php b/app/Models/Attribute.php
new file mode 100644
index 000000000..c090e2c12
--- /dev/null
+++ b/app/Models/Attribute.php
@@ -0,0 +1,59 @@
+ AttributeType::class,
+ 'global' => 'boolean',
+ 'sortable' => 'boolean',
+ 'created_at' => 'datetime',
+ 'updated_at' => 'datetime',
+ ];
+
+ protected array $criteria = [
+ 'global',
+ ];
+
+ public function options(): HasMany
+ {
+ return $this->hasMany(AttributeOption::class);
+ }
+
+ public function products(): BelongsToMany
+ {
+ return $this->belongsToMany(Product::class, 'product_attribute')
+ ->withPivot('id')
+ ->using(ProductAttribute::class);
+ }
+
+ public function productSets(): BelongsToMany
+ {
+ return $this->belongsToMany(ProductSet::class);
+ }
+}
diff --git a/app/Models/AttributeOption.php b/app/Models/AttributeOption.php
new file mode 100644
index 000000000..f7be65e1e
--- /dev/null
+++ b/app/Models/AttributeOption.php
@@ -0,0 +1,39 @@
+ 'datetime',
+ 'updated_at' => 'datetime',
+ ];
+
+ public function attribute(): BelongsTo
+ {
+ return $this->belongsTo(Attribute::class);
+ }
+
+ public function productAttributes(): BelongsToMany
+ {
+ return $this->belongsToMany(ProductAttribute::class);
+ }
+}
diff --git a/app/Models/Discount.php b/app/Models/Discount.php
index 920adb5a0..f49c7607a 100644
--- a/app/Models/Discount.php
+++ b/app/Models/Discount.php
@@ -2,11 +2,14 @@
namespace App\Models;
+use App\Criteria\DiscountSearch;
+use App\Criteria\MetadataPrivateSearch;
+use App\Criteria\MetadataSearch;
use App\Enums\DiscountType;
-use App\SearchTypes\DiscountSearch;
+use App\Traits\HasMetadata;
use Carbon\Carbon;
-use Heseya\Searchable\Searches\Like;
-use Heseya\Searchable\Traits\Searchable;
+use Heseya\Searchable\Criteria\Like;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -18,7 +21,7 @@
*/
class Discount extends Model implements AuditableContract
{
- use HasFactory, Searchable, SoftDeletes, Auditable;
+ use HasFactory, HasCriteria, SoftDeletes, Auditable, HasMetadata;
protected $fillable = [
'description',
@@ -32,7 +35,6 @@ class Discount extends Model implements AuditableContract
protected $casts = [
'type' => DiscountType::class,
-
];
protected $dates = [
@@ -40,10 +42,12 @@ class Discount extends Model implements AuditableContract
'expires_at',
];
- protected array $searchable = [
+ protected array $criteria = [
'description' => Like::class,
'code' => Like::class,
'search' => DiscountSearch::class,
+ 'metadata' => MetadataSearch::class,
+ 'metadata_private' => MetadataPrivateSearch::class,
];
public function getUsesAttribute(): int
diff --git a/app/Models/Item.php b/app/Models/Item.php
index 07d6d382d..c4e46b9fe 100644
--- a/app/Models/Item.php
+++ b/app/Models/Item.php
@@ -2,12 +2,15 @@
namespace App\Models;
-use App\SearchTypes\ItemSearch;
-use App\SearchTypes\WhereCreatedBefore;
-use App\SearchTypes\WhereSoldOut;
-use Heseya\Searchable\Searches\Like;
-use Heseya\Searchable\Traits\Searchable;
-use Heseya\Sortable\Sortable;
+use App\Criteria\ItemSearch;
+use App\Criteria\MetadataPrivateSearch;
+use App\Criteria\MetadataSearch;
+use App\Criteria\WhereCreatedBefore;
+use App\Criteria\WhereSoldOut;
+use App\Traits\HasMetadata;
+use App\Traits\Sortable;
+use Heseya\Searchable\Criteria\Like;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -21,7 +24,7 @@
*/
class Item extends Model implements AuditableContract
{
- use SoftDeletes, HasFactory, Searchable, Sortable, Auditable;
+ use SoftDeletes, HasFactory, HasCriteria, Sortable, Auditable, HasMetadata;
protected $fillable = [
'name',
@@ -29,12 +32,14 @@ class Item extends Model implements AuditableContract
'quantity',
];
- protected array $searchable = [
+ protected array $criteria = [
'name' => Like::class,
'sku' => Like::class,
'search' => ItemSearch::class,
'sold_out' => WhereSoldOut::class,
'day' => WhereCreatedBefore::class,
+ 'metadata' => MetadataSearch::class,
+ 'metadata_private' => MetadataPrivateSearch::class,
];
protected array $sortable = [
diff --git a/app/Models/Media.php b/app/Models/Media.php
index cc7ababbf..bf7b227cd 100644
--- a/app/Models/Media.php
+++ b/app/Models/Media.php
@@ -3,6 +3,7 @@
namespace App\Models;
use App\Enums\MediaType;
+use App\Traits\HasMetadata;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -11,7 +12,7 @@
*/
class Media extends Model
{
- use HasFactory;
+ use HasFactory, HasMetadata;
/**
* The table associated with the model.
diff --git a/app/Models/Metadata.php b/app/Models/Metadata.php
new file mode 100644
index 000000000..348af853f
--- /dev/null
+++ b/app/Models/Metadata.php
@@ -0,0 +1,41 @@
+ MetadataValue::class,
+ 'value_type' => MetadataType::class,
+ 'public' => 'boolean',
+ 'created_at' => 'datetime',
+ 'updated_at' => 'datetime',
+ ];
+
+ public function scopePublic($query): Builder
+ {
+ return $query->where('public', true);
+ }
+
+ public function scopePrivate($query): Builder
+ {
+ return $query->where('public', false);
+ }
+}
diff --git a/app/Models/Option.php b/app/Models/Option.php
index af6fd92ce..ad8835e80 100644
--- a/app/Models/Option.php
+++ b/app/Models/Option.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\HasMetadata;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -11,7 +12,7 @@
*/
class Option extends Model
{
- use HasFactory;
+ use HasFactory, HasMetadata;
protected $fillable = [
'name',
diff --git a/app/Models/Order.php b/app/Models/Order.php
index 69d12d16f..3ade55686 100644
--- a/app/Models/Order.php
+++ b/app/Models/Order.php
@@ -5,13 +5,16 @@
use App\Audits\Redactors\AddressRedactor;
use App\Audits\Redactors\ShippingMethodRedactor;
use App\Audits\Redactors\StatusRedactor;
-use App\SearchTypes\OrderSearch;
-use App\SearchTypes\WhereCreatedAfter;
-use App\SearchTypes\WhereCreatedBefore;
-use App\SearchTypes\WhereHasStatusHidden;
-use Heseya\Searchable\Searches\Like;
-use Heseya\Searchable\Traits\Searchable;
-use Heseya\Sortable\Sortable;
+use App\Criteria\MetadataPrivateSearch;
+use App\Criteria\MetadataSearch;
+use App\Criteria\OrderSearch;
+use App\Criteria\WhereCreatedAfter;
+use App\Criteria\WhereCreatedBefore;
+use App\Criteria\WhereHasStatusHidden;
+use App\Traits\HasMetadata;
+use App\Traits\Sortable;
+use Heseya\Searchable\Criteria\Like;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -29,7 +32,7 @@
*/
class Order extends Model implements AuditableContract
{
- use HasFactory, Searchable, Sortable, Notifiable, Auditable;
+ use HasFactory, HasCriteria, Sortable, Notifiable, Auditable, HasMetadata;
protected $fillable = [
'code',
@@ -69,7 +72,7 @@ class Order extends Model implements AuditableContract
'invoice_address_id' => AddressRedactor::class,
];
- protected array $searchable = [
+ protected array $criteria = [
'search' => OrderSearch::class,
'status_id',
'shipping_method_id',
@@ -80,6 +83,8 @@ class Order extends Model implements AuditableContract
'paid',
'from' => WhereCreatedAfter::class,
'to' => WhereCreatedBefore::class,
+ 'metadata' => MetadataSearch::class,
+ 'metadata_private' => MetadataPrivateSearch::class,
];
protected array $sortable = [
diff --git a/app/Models/PackageTemplate.php b/app/Models/PackageTemplate.php
index 8f9e00b69..19539a872 100644
--- a/app/Models/PackageTemplate.php
+++ b/app/Models/PackageTemplate.php
@@ -2,6 +2,10 @@
namespace App\Models;
+use App\Criteria\MetadataPrivateSearch;
+use App\Criteria\MetadataSearch;
+use App\Traits\HasMetadata;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Database\Eloquent\Factories\HasFactory;
/**
@@ -9,7 +13,7 @@
*/
class PackageTemplate extends Model
{
- use HasFactory;
+ use HasFactory, HasCriteria, HasMetadata;
protected $fillable = [
'name',
@@ -22,4 +26,9 @@ class PackageTemplate extends Model
protected $casts = [
'weight' => 'float',
];
+
+ protected array $criteria = [
+ 'metadata' => MetadataSearch::class,
+ 'metadata_private' => MetadataPrivateSearch::class,
+ ];
}
diff --git a/app/Models/Page.php b/app/Models/Page.php
index b2dca0686..ccb03c080 100644
--- a/app/Models/Page.php
+++ b/app/Models/Page.php
@@ -2,8 +2,12 @@
namespace App\Models;
+use App\Criteria\MetadataPrivateSearch;
+use App\Criteria\MetadataSearch;
+use App\Traits\HasMetadata;
use App\Traits\HasSeoMetadata;
-use Heseya\Sortable\Sortable;
+use App\Traits\Sortable;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use OwenIt\Auditing\Auditable;
@@ -14,7 +18,7 @@
*/
class Page extends Model implements AuditableContract
{
- use HasFactory, Sortable, Auditable, SoftDeletes, HasSeoMetadata;
+ use HasFactory, HasCriteria, Sortable, Auditable, SoftDeletes, HasSeoMetadata, HasMetadata;
protected $fillable = [
'order',
@@ -33,4 +37,9 @@ class Page extends Model implements AuditableContract
'created_at',
'updated_at',
];
+
+ protected array $criteria = [
+ 'metadata' => MetadataSearch::class,
+ 'metadata_private' => MetadataPrivateSearch::class,
+ ];
}
diff --git a/app/Models/Permission.php b/app/Models/Permission.php
index 05ff6661c..0ebae5b8f 100644
--- a/app/Models/Permission.php
+++ b/app/Models/Permission.php
@@ -2,9 +2,9 @@
namespace App\Models;
-use App\SearchTypes\PermissionSearch;
+use App\Criteria\PermissionSearch;
use App\Traits\HasUuid;
-use Heseya\Searchable\Traits\Searchable;
+use Heseya\Searchable\Traits\HasCriteria;
use Spatie\Permission\Models\Permission as SpatiePermission;
/**
@@ -12,7 +12,7 @@
*/
class Permission extends SpatiePermission
{
- use Searchable, HasUuid;
+ use HasCriteria, HasUuid;
protected $fillable = [
'name',
@@ -21,7 +21,7 @@ class Permission extends SpatiePermission
'guard_name',
];
- protected array $searchable = [
+ protected array $criteria = [
'assignable' => PermissionSearch::class,
];
}
diff --git a/app/Models/Product.php b/app/Models/Product.php
index 348270710..a4227d05d 100644
--- a/app/Models/Product.php
+++ b/app/Models/Product.php
@@ -2,26 +2,25 @@
namespace App\Models;
-use App\SearchTypes\ProductSearch;
-use App\SearchTypes\WhereBelongsToManyById;
-use App\SearchTypes\WhereInIds;
+use App\Services\Contracts\ProductSearchServiceContract;
+use App\Traits\HasMetadata;
use App\Traits\HasSeoMetadata;
-use Heseya\Searchable\Searches\Like;
-use Heseya\Searchable\Traits\Searchable;
-use Heseya\Sortable\Sortable;
+use App\Traits\Sortable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
+use JeroenG\Explorer\Application\Explored;
+use Laravel\Scout\Searchable;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
/**
* @mixin IdeHelperProduct
*/
-class Product extends Model implements AuditableContract
+class Product extends Model implements AuditableContract, Explored
{
- use HasFactory, SoftDeletes, Searchable, Sortable, Auditable, HasSeoMetadata;
+ use HasFactory, SoftDeletes, Searchable, Sortable, Auditable, HasSeoMetadata, HasMetadata;
protected $fillable = [
'name',
@@ -41,11 +40,14 @@ class Product extends Model implements AuditableContract
protected $auditInclude = [
'name',
'slug',
- 'price',
'description_html',
'description_short',
'public',
'quantity_step',
+ 'price_min',
+ 'price_max',
+ 'available',
+ 'order',
];
protected $casts = [
@@ -55,15 +57,6 @@ class Product extends Model implements AuditableContract
'quantity_step' => 'float',
];
- protected array $searchable = [
- 'ids' => WhereInIds::class,
- 'name' => Like::class,
- 'slug' => Like::class,
- 'public',
- 'search' => ProductSearch::class,
- 'tags' => WhereBelongsToManyById::class,
- ];
-
protected array $sortable = [
'id',
'price',
@@ -72,11 +65,32 @@ class Product extends Model implements AuditableContract
'updated_at',
'order',
'public',
+ 'available',
];
- protected string $defaultSortBy = 'created_at';
+ protected string $defaultSortBy = 'order';
+
protected string $defaultSortDirection = 'desc';
+ private ProductSearchServiceContract $searchService;
+
+ public function __construct(array $attributes = [])
+ {
+ parent::__construct($attributes);
+
+ $this->searchService = app(ProductSearchServiceContract::class);
+ }
+
+ public function toSearchableArray(): array
+ {
+ return $this->searchService->mapSearchableArray($this);
+ }
+
+ public function sets(): BelongsToMany
+ {
+ return $this->belongsToMany(ProductSet::class, 'product_set_product');
+ }
+
public function items(): BelongsToMany
{
return $this
@@ -104,6 +118,11 @@ public function tags(): BelongsToMany
return $this->belongsToMany(Tag::class, 'product_tags');
}
+ public function requiredSchemas(): BelongsToMany
+ {
+ return $this->schemas()->where('required', true);
+ }
+
public function schemas(): BelongsToMany
{
return $this
@@ -111,18 +130,38 @@ public function schemas(): BelongsToMany
->orderByPivot('order');
}
- public function requiredSchemas(): BelongsToMany
+ public function scopePublic($query): Builder
{
- return $this->schemas()->where('required', true);
+ return $query->where('public', true);
}
- public function sets(): BelongsToMany
+ public function attributes(): BelongsToMany
{
- return $this->belongsToMany(ProductSet::class, 'product_set_product');
+ return $this->belongsToMany(Attribute::class, 'product_attribute')
+ ->withPivot('id')
+ ->using(ProductAttribute::class);
}
- public function scopePublic($query): Builder
+ public function mappableAs(): array
{
- return $query->where('public', true);
+ return [];
+ }
+
+ public function indexSettings(): array
+ {
+ return [
+ 'analysis' => [
+ 'analyzer' => [
+ 'standard_lowercase' => [
+ 'type' => 'custom',
+ 'tokenizer' => 'whitespace',
+ 'filter' => [
+ 'lowercase',
+ 'morfologik_stem',
+ ],
+ ],
+ ],
+ ],
+ ];
}
}
diff --git a/app/Models/ProductAttribute.php b/app/Models/ProductAttribute.php
new file mode 100644
index 000000000..47e3ca8c1
--- /dev/null
+++ b/app/Models/ProductAttribute.php
@@ -0,0 +1,36 @@
+belongsToMany(
+ AttributeOption::class,
+ 'product_attribute_attribute_option',
+ 'product_attribute_id',
+ 'attribute_option_id',
+ );
+ }
+
+ public function product(): BelongsTo
+ {
+ return $this->belongsTo(Product::class);
+ }
+
+ public function attribute(): BelongsTo
+ {
+ return $this->belongsTo(Attribute::class);
+ }
+}
diff --git a/app/Models/ProductSet.php b/app/Models/ProductSet.php
index 697897040..e5968b8bc 100644
--- a/app/Models/ProductSet.php
+++ b/app/Models/ProductSet.php
@@ -2,10 +2,13 @@
namespace App\Models;
-use App\SearchTypes\ProductSetSearch;
+use App\Criteria\MetadataPrivateSearch;
+use App\Criteria\MetadataSearch;
+use App\Criteria\ProductSetSearch;
+use App\Traits\HasMetadata;
use App\Traits\HasSeoMetadata;
-use Heseya\Searchable\Searches\Like;
-use Heseya\Searchable\Traits\Searchable;
+use Heseya\Searchable\Criteria\Like;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -20,7 +23,7 @@
*/
class ProductSet extends Model
{
- use Searchable, HasFactory, SoftDeletes, HasSeoMetadata;
+ use HasCriteria, HasFactory, SoftDeletes, HasSeoMetadata, HasMetadata;
protected $fillable = [
'name',
@@ -40,11 +43,13 @@ class ProductSet extends Model
'hide_on_index' => 'boolean',
];
- protected array $searchable = [
+ protected array $criteria = [
'name' => Like::class,
'slug' => Like::class,
'search' => ProductSetSearch::class,
'public',
+ 'metadata' => MetadataSearch::class,
+ 'metadata_private' => MetadataPrivateSearch::class,
];
public function getSlugOverrideAttribute(): bool
@@ -55,6 +60,11 @@ public function getSlugOverrideAttribute(): bool
);
}
+ public function parent(): BelongsTo
+ {
+ return $this->belongsTo(self::class, 'parent_id');
+ }
+
public function getSlugSuffixAttribute(): string
{
return $this->slugOverride || $this->parent()->doesntExist() ? $this->slug :
@@ -77,9 +87,9 @@ public function scopeReversed($query): Builder
->orderBy('order', 'desc');
}
- public function parent(): BelongsTo
+ public function allChildren(): HasMany
{
- return $this->belongsTo(self::class, 'parent_id');
+ return $this->children()->with('allChildren');
}
public function children(): HasMany
@@ -87,19 +97,19 @@ public function children(): HasMany
return $this->hasMany(self::class, 'parent_id');
}
- public function childrenPublic(): HasMany
+ public function attributes(): BelongsToMany
{
- return $this->children()->public();
+ return $this->belongsToMany(Attribute::class);
}
- public function allChildren(): HasMany
+ public function allChildrenPublic(): HasMany
{
- return $this->children()->with('allChildren');
+ return $this->childrenPublic()->with('allChildrenPublic');
}
- public function allChildrenPublic(): HasMany
+ public function childrenPublic(): HasMany
{
- return $this->childrenPublic()->with('allChildrenPublic');
+ return $this->children()->public();
}
public function products(): BelongsToMany
diff --git a/app/Models/Role.php b/app/Models/Role.php
index ad7a6b853..a8e45497e 100644
--- a/app/Models/Role.php
+++ b/app/Models/Role.php
@@ -2,12 +2,15 @@
namespace App\Models;
+use App\Criteria\MetadataPrivateSearch;
+use App\Criteria\MetadataSearch;
+use App\Criteria\RoleAssignableSearch;
+use App\Criteria\RoleSearch;
use App\Enums\RoleType;
-use App\SearchTypes\RoleAssignableSearch;
-use App\SearchTypes\RoleSearch;
+use App\Traits\HasMetadata;
use App\Traits\HasUuid;
-use Heseya\Searchable\Searches\Like;
-use Heseya\Searchable\Traits\Searchable;
+use Heseya\Searchable\Criteria\Like;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use OwenIt\Auditing\Auditable;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
@@ -18,7 +21,7 @@
*/
class Role extends SpatieRole implements AuditableContract
{
- use Searchable, HasUuid, HasFactory, Auditable;
+ use HasCriteria, HasUuid, HasFactory, Auditable, HasMetadata;
protected $fillable = [
'name',
@@ -30,10 +33,12 @@ class Role extends SpatieRole implements AuditableContract
'type' => RoleType::class,
];
- protected array $searchable = [
+ protected array $criteria = [
'name' => Like::class,
'description' => Like::class,
'search' => RoleSearch::class,
'assignable' => RoleAssignableSearch::class,
+ 'metadata' => MetadataSearch::class,
+ 'metadata_private' => MetadataPrivateSearch::class,
];
}
diff --git a/app/Models/Schema.php b/app/Models/Schema.php
index 0239230f8..fa4c4b206 100644
--- a/app/Models/Schema.php
+++ b/app/Models/Schema.php
@@ -2,13 +2,16 @@
namespace App\Models;
+use App\Criteria\MetadataPrivateSearch;
+use App\Criteria\MetadataSearch;
+use App\Criteria\SchemaSearch;
use App\Enums\SchemaType;
use App\Rules\OptionAvailable;
-use App\SearchTypes\SchemaSearch;
+use App\Traits\HasMetadata;
+use App\Traits\Sortable;
use BenSampo\Enum\Exceptions\InvalidEnumKeyException;
-use Heseya\Searchable\Searches\Like;
-use Heseya\Searchable\Traits\Searchable;
-use Heseya\Sortable\Sortable;
+use Heseya\Searchable\Criteria\Like;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -23,7 +26,7 @@
*/
class Schema extends Model
{
- use HasFactory, Searchable, Sortable;
+ use HasFactory, HasCriteria, Sortable, HasMetadata;
protected $fillable = [
'type',
@@ -49,11 +52,13 @@ class Schema extends Model
'type' => SchemaType::class,
];
- protected $searchable = [
+ protected $criteria = [
'search' => SchemaSearch::class,
'name' => Like::class,
'hidden',
'required',
+ 'metadata' => MetadataSearch::class,
+ 'metadata_private' => MetadataPrivateSearch::class,
];
protected array $sortable = [
diff --git a/app/Models/ShippingMethod.php b/app/Models/ShippingMethod.php
index 94eb6b496..972eedadc 100644
--- a/app/Models/ShippingMethod.php
+++ b/app/Models/ShippingMethod.php
@@ -2,6 +2,10 @@
namespace App\Models;
+use App\Criteria\MetadataPrivateSearch;
+use App\Criteria\MetadataSearch;
+use App\Traits\HasMetadata;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -13,7 +17,7 @@
*/
class ShippingMethod extends Model implements AuditableContract
{
- use HasFactory, Auditable;
+ use HasFactory, Auditable, HasCriteria, HasMetadata;
/**
* The attributes that are mass assignable.
@@ -37,6 +41,11 @@ class ShippingMethod extends Model implements AuditableContract
'black_list' => 'boolean',
];
+ protected array $criteria = [
+ 'metadata' => MetadataSearch::class,
+ 'metadata_private' => MetadataPrivateSearch::class,
+ ];
+
public function orders(): HasMany
{
return $this->hasMany(Order::class);
diff --git a/app/Models/Status.php b/app/Models/Status.php
index 7c699f3b5..de22e9b8b 100644
--- a/app/Models/Status.php
+++ b/app/Models/Status.php
@@ -2,6 +2,10 @@
namespace App\Models;
+use App\Criteria\MetadataPrivateSearch;
+use App\Criteria\MetadataSearch;
+use App\Traits\HasMetadata;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use OwenIt\Auditing\Auditable;
@@ -12,7 +16,7 @@
*/
class Status extends Model implements AuditableContract
{
- use HasFactory, Auditable;
+ use HasFactory, HasCriteria, Auditable, HasMetadata;
protected $fillable = [
'name',
@@ -35,6 +39,11 @@ class Status extends Model implements AuditableContract
'no_notifications' => false,
];
+ protected array $criteria = [
+ 'metadata' => MetadataSearch::class,
+ 'metadata_private' => MetadataPrivateSearch::class,
+ ];
+
public function orders(): HasMany
{
return $this->hasMany(Order::class);
diff --git a/app/Models/Tag.php b/app/Models/Tag.php
index 121273919..878281c82 100644
--- a/app/Models/Tag.php
+++ b/app/Models/Tag.php
@@ -2,9 +2,9 @@
namespace App\Models;
-use App\SearchTypes\TagSearch;
-use Heseya\Searchable\Searches\Like;
-use Heseya\Searchable\Traits\Searchable;
+use App\Criteria\TagSearch;
+use Heseya\Searchable\Criteria\Like;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -13,14 +13,14 @@
*/
class Tag extends Model
{
- use HasFactory, Searchable;
+ use HasFactory, HasCriteria;
protected $fillable = [
'name',
'color',
];
- protected array $searchable = [
+ protected array $criteria = [
'name' => Like::class,
'color' => Like::class,
'search' => TagSearch::class,
diff --git a/app/Models/User.php b/app/Models/User.php
index 31687b43d..268355e92 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -2,12 +2,15 @@
namespace App\Models;
-use App\SearchTypes\UserSearch;
-use App\SearchTypes\WhereInIds;
+use App\Criteria\MetadataPrivateSearch;
+use App\Criteria\MetadataSearch;
+use App\Criteria\UserSearch;
+use App\Criteria\WhereInIds;
+use App\Traits\HasMetadata;
use App\Traits\HasWebHooks;
-use Heseya\Searchable\Searches\Like;
-use Heseya\Searchable\Traits\Searchable;
-use Heseya\Sortable\Sortable;
+use App\Traits\Sortable;
+use Heseya\Searchable\Criteria\Like;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\MustVerifyEmail;
use Illuminate\Auth\Passwords\CanResetPassword;
@@ -43,10 +46,11 @@ class User extends Model implements
HasFactory,
HasRoles,
SoftDeletes,
- Searchable,
+ HasCriteria,
Sortable,
Auditable,
- HasWebHooks;
+ HasWebHooks,
+ HasMetadata;
// Bez tego nie działały testy, w których jako aplikacja tworzy się użytkownika z określoną rolą
protected $guard_name = 'api';
@@ -65,11 +69,13 @@ class User extends Model implements
'remember_token',
];
- protected array $searchable = [
+ protected array $criteria = [
'name' => Like::class,
'email' => Like::class,
'search' => UserSearch::class,
'ids' => WhereInIds::class,
+ 'metadata' => MetadataSearch::class,
+ 'metadata_private' => MetadataPrivateSearch::class,
];
protected array $sortable = [
diff --git a/app/Models/WebHook.php b/app/Models/WebHook.php
index 62d6d5da6..e6a99b1c9 100644
--- a/app/Models/WebHook.php
+++ b/app/Models/WebHook.php
@@ -2,9 +2,9 @@
namespace App\Models;
-use Heseya\Searchable\Searches\Like;
-use Heseya\Searchable\Traits\Searchable;
-use Heseya\Sortable\Sortable;
+use App\Traits\Sortable;
+use Heseya\Searchable\Criteria\Like;
+use Heseya\Searchable\Traits\HasCriteria;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
@@ -18,7 +18,7 @@
*/
class WebHook extends Model implements AuditableContract
{
- use HasFactory, SoftDeletes, Searchable, Sortable, Auditable, Notifiable;
+ use HasFactory, SoftDeletes, HasCriteria, Sortable, Auditable, Notifiable;
protected $fillable = [
'name',
@@ -39,7 +39,7 @@ class WebHook extends Model implements AuditableContract
'events' => 'array',
];
- protected array $searchable = [
+ protected array $criteria = [
'name' => Like::class,
'url' => Like::class,
];
diff --git a/app/Observers/AttributeOptionObserver.php b/app/Observers/AttributeOptionObserver.php
new file mode 100644
index 000000000..dd8581275
--- /dev/null
+++ b/app/Observers/AttributeOptionObserver.php
@@ -0,0 +1,61 @@
+attributeService->updateMinMax($attributeOption->attribute);
+ }
+
+ /**
+ * Handle the AttributeOption "updated" event.
+ *
+ * @param AttributeOption $attributeOption
+ *
+ * @return void
+ */
+ public function updated(AttributeOption $attributeOption): void
+ {
+ $this->attributeService->updateMinMax($attributeOption->attribute);
+ }
+
+ /**
+ * Handle the AttributeOption "deleted" event.
+ *
+ * @param AttributeOption $attributeOption
+ *
+ * @return void
+ */
+ public function deleted(AttributeOption $attributeOption): void
+ {
+ $this->attributeService->updateMinMax($attributeOption->attribute);
+ }
+
+ /**
+ * Handle the AttributeOption "restored" event.
+ *
+ * @param AttributeOption $attributeOption
+ *
+ * @return void
+ */
+ public function restored(AttributeOption $attributeOption): void
+ {
+ $this->attributeService->updateMinMax($attributeOption->attribute);
+ }
+}
diff --git a/app/Observers/ProductObserver.php b/app/Observers/ProductObserver.php
deleted file mode 100644
index 99485ab18..000000000
--- a/app/Observers/ProductObserver.php
+++ /dev/null
@@ -1,21 +0,0 @@
-availabilityService = $availabilityService;
- }
-
- public function created(Product $product)
- {
- $this->availabilityService->calculateProductAvailability($product);
- }
-}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index f73bd5a89..6678ad8e3 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -4,11 +4,15 @@
use App\Services\AnalyticsService;
use App\Services\AppService;
+use App\Services\AttributeOptionService;
+use App\Services\AttributeService;
use App\Services\AuditService;
use App\Services\AuthService;
use App\Services\AvailabilityService;
use App\Services\Contracts\AnalyticsServiceContract;
use App\Services\Contracts\AppServiceContract;
+use App\Services\Contracts\AttributeOptionServiceContract;
+use App\Services\Contracts\AttributeServiceContract;
use App\Services\Contracts\AuditServiceContract;
use App\Services\Contracts\AuthServiceContract;
use App\Services\Contracts\AvailabilityServiceContract;
@@ -16,12 +20,14 @@
use App\Services\Contracts\EventServiceContract;
use App\Services\Contracts\ItemServiceContract;
use App\Services\Contracts\MediaServiceContract;
+use App\Services\Contracts\MetadataServiceContract;
use App\Services\Contracts\NameServiceContract;
use App\Services\Contracts\OneTimeSecurityCodeContract;
use App\Services\Contracts\OptionServiceContract;
use App\Services\Contracts\OrderServiceContract;
use App\Services\Contracts\PageServiceContract;
use App\Services\Contracts\PermissionServiceContract;
+use App\Services\Contracts\ProductSearchServiceContract;
use App\Services\Contracts\ProductServiceContract;
use App\Services\Contracts\ProductSetServiceContract;
use App\Services\Contracts\ReorderServiceContract;
@@ -30,6 +36,7 @@
use App\Services\Contracts\SeoMetadataServiceContract;
use App\Services\Contracts\SettingsServiceContract;
use App\Services\Contracts\ShippingMethodServiceContract;
+use App\Services\Contracts\SortServiceContract;
use App\Services\Contracts\TokenServiceContract;
use App\Services\Contracts\UrlServiceContract;
use App\Services\Contracts\UserServiceContract;
@@ -38,12 +45,14 @@
use App\Services\EventService;
use App\Services\ItemService;
use App\Services\MediaService;
+use App\Services\MetadataService;
use App\Services\NameService;
use App\Services\OneTimeSecurityCodeService;
use App\Services\OptionService;
use App\Services\OrderService;
use App\Services\PageService;
use App\Services\PermissionService;
+use App\Services\ProductSearchService;
use App\Services\ProductService;
use App\Services\ProductSetService;
use App\Services\ReorderService;
@@ -52,11 +61,13 @@
use App\Services\SeoMetadataService;
use App\Services\SettingsService;
use App\Services\ShippingMethodService;
+use App\Services\SortService;
use App\Services\TokenService;
use App\Services\UrlService;
use App\Services\UserService;
use App\Services\WebHookService;
use Illuminate\Support\ServiceProvider;
+use Laravel\Scout\Builder;
class AppServiceProvider extends ServiceProvider
{
@@ -88,6 +99,11 @@ class AppServiceProvider extends ServiceProvider
ItemServiceContract::class => ItemService::class,
OneTimeSecurityCodeContract::class => OneTimeSecurityCodeService::class,
AvailabilityServiceContract::class => AvailabilityService::class,
+ MetadataServiceContract::class => MetadataService::class,
+ AttributeServiceContract::class => AttributeService::class,
+ AttributeOptionServiceContract::class => AttributeOptionService::class,
+ SortServiceContract::class => SortService::class,
+ ProductSearchServiceContract::class => ProductSearchService::class,
];
/**
@@ -110,6 +126,12 @@ public function register(): void
public function boot(): void
{
- // Model::preventLazyLoading();
+ Builder::macro('sort', function (?string $sortString = null) {
+ if ($sortString !== null) {
+ return app(SortServiceContract::class)->sort($this, $sortString);
+ }
+
+ return $this;
+ });
}
}
diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php
index d40d13a6b..931591dcd 100644
--- a/app/Providers/EventServiceProvider.php
+++ b/app/Providers/EventServiceProvider.php
@@ -28,16 +28,15 @@
use App\Listeners\OrderCreatedListener;
use App\Listeners\OrderUpdatedStatusListener;
use App\Listeners\WebHookEventListener;
+use App\Models\AttributeOption;
use App\Models\Deposit;
use App\Models\ItemProduct;
use App\Models\Payment;
-use App\Models\Product;
+use App\Observers\AttributeOptionObserver;
use App\Observers\DepositObserver;
use App\Observers\ItemProductObserver;
use App\Observers\PaymentObserver;
-use App\Observers\ProductObserver;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
-use OwenIt\Auditing\AuditableObserver;
class EventServiceProvider extends ServiceProvider
{
@@ -124,7 +123,7 @@ public function boot(): void
parent::boot();
Payment::observe(PaymentObserver::class);
Deposit::observe(DepositObserver::class);
- Product::observe([ProductObserver::class, AuditableObserver::class]);
+ AttributeOption::observe(AttributeOptionObserver::class);
ItemProduct::observe(ItemProductObserver::class);
}
}
diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php
new file mode 100644
index 000000000..069704d7d
--- /dev/null
+++ b/app/Providers/RepositoryServiceProvider.php
@@ -0,0 +1,21 @@
+ ProductRepository::class,
+ ];
+
+ public function register(): void
+ {
+ foreach (self::CONTRACTS as $abstract => $concrete) {
+ $this->app->bind($abstract, $concrete);
+ }
+ }
+}
diff --git a/app/Repositories/Contracts/ProductRepositoryContract.php b/app/Repositories/Contracts/ProductRepositoryContract.php
new file mode 100644
index 000000000..3e13abd1b
--- /dev/null
+++ b/app/Repositories/Contracts/ProductRepositoryContract.php
@@ -0,0 +1,11 @@
+ 'filterIds',
+ 'slug' => 'must',
+ 'name' => 'must',
+ 'public' => 'filter',
+ 'available' => 'filter',
+ 'sets' => 'filterSlug',
+ 'tags' => 'filterId',
+ 'metadata' => 'filterMeta',
+ 'metadata_private' => 'filterMeta',
+ ];
+
+ public function search(ProductSearchDto $dto): LengthAwarePaginator
+ {
+ $query = Product::search($dto->getSearch())
+ ->sort($dto->getSort());
+
+ $hide_on_index = true;
+
+ foreach ($dto->toArray() as $key => $value) {
+ if (array_key_exists($key, self::CRITERIA)) {
+ $query = $this->{self::CRITERIA[$key]}($query, $key, $value);
+ $hide_on_index = false;
+ }
+ }
+
+ if (Gate::denies('products.show_hidden')) {
+ $query->filter(new Term('public', true));
+
+ if ($hide_on_index && $dto->getSearch() === null) {
+ $query->filter(new Term('hide_on_index', false));
+ }
+ }
+
+ return $query->paginate(Config::get('pagination.per_page'));
+ }
+
+ private function must(Builder $query, string $key, string|int|float|bool $value): Builder
+ {
+ return $query->must(new Matching($key, $value));
+ }
+
+ private function filter(Builder $query, string $key, string|int|float|bool $value): Builder
+ {
+ return $query->filter(new Term($key, $value));
+ }
+
+ private function filterSlug(Builder $query, string $key, array $slugs): Builder
+ {
+ return $query->filter(new Terms("{$key}.slug", $slugs));
+ }
+
+ private function filterId(Builder $query, string $key, array $ids): Builder
+ {
+ return $query->filter(new Terms("{$key}.id", $ids));
+ }
+
+ private function filterIds(Builder $query, string $key, string $ids): Builder
+ {
+ $ids = Str::of($ids)->explode(',');
+
+ $query->filter(new Terms('id', $ids->toArray()));
+
+ return $query;
+ }
+
+ private function filterMeta(Builder $query, string $key, array $meta): Builder
+ {
+ $query->filter(new Terms("${key}.name", array_keys($meta)));
+ $query->filter(new Terms("${key}.value", array_values($meta)));
+
+ return $query;
+ }
+}
diff --git a/app/Rules/AttributeOptionExist.php b/app/Rules/AttributeOptionExist.php
new file mode 100644
index 000000000..778a76154
--- /dev/null
+++ b/app/Rules/AttributeOptionExist.php
@@ -0,0 +1,26 @@
+attributeId = Str::between($attribute, '.', '.');
+
+ $attributeOption = AttributeOption::where('id', $value)->where('attribute_id', $this->attributeId)->first();
+
+ return $attributeOption !== null;
+ }
+
+ public function message()
+ {
+ return 'This option :value is not available in attribute :attribute';
+ }
+}
diff --git a/app/Rules/CanShowPrivateMetadata.php b/app/Rules/CanShowPrivateMetadata.php
new file mode 100644
index 000000000..4bb18c083
--- /dev/null
+++ b/app/Rules/CanShowPrivateMetadata.php
@@ -0,0 +1,32 @@
+message = 'Attribute :attribute not found';
+ return false;
+ }
+
+ if ($attributeModel->type->is(AttributeType::MULTI_CHOICE_OPTION)) {
+ $this->message = 'Attribute :attribute must have at least one option';
+ return count($value) >= 1;
+ }
+
+ $this->message = 'Attribute :attribute must have one option';
+ return count($value) === 1;
+ }
+
+ public function message()
+ {
+ return $this->message;
+ }
+}
diff --git a/app/SearchTypes/ProductSearch.php b/app/SearchTypes/ProductSearch.php
deleted file mode 100644
index 4ae097239..000000000
--- a/app/SearchTypes/ProductSearch.php
+++ /dev/null
@@ -1,19 +0,0 @@
-where(function (Builder $query): void {
- $query->where('id', 'LIKE', '%' . $this->value . '%')
- ->orWhere('slug', 'LIKE', '%' . $this->value . '%')
- ->orWhere('name', 'LIKE', '%' . $this->value . '%')
- ->orWhere('description_html', 'LIKE', '%' . $this->value . '%');
- });
- }
-}
diff --git a/app/SearchTypes/WhereHasStatusHidden.php b/app/SearchTypes/WhereHasStatusHidden.php
deleted file mode 100644
index 516c01d1d..000000000
--- a/app/SearchTypes/WhereHasStatusHidden.php
+++ /dev/null
@@ -1,22 +0,0 @@
-whereHas('status', function (Builder $query) {
- return $query->where('hidden', $this->value);
- });
-
- if (!$this->value) {
- $query->orWhereDoesntHave('status');
- }
-
- return $query;
- }
-}
diff --git a/app/Services/AttributeOptionService.php b/app/Services/AttributeOptionService.php
new file mode 100644
index 000000000..bb6d8b712
--- /dev/null
+++ b/app/Services/AttributeOptionService.php
@@ -0,0 +1,48 @@
+ AttributeOption::withTrashed()->where('attribute_id', '=', $attributeId)->count() + 1,
+ 'attribute_id' => $attributeId,
+ ],
+ $dto->toArray(),
+ );
+
+ return AttributeOption::create($data);
+ }
+
+ public function updateOrCreate(string $attributeId, AttributeOptionDto $dto): AttributeOption
+ {
+ if ($dto->getId() !== null && !$dto->getId() instanceof Missing) {
+ $attributeOption = AttributeOption::findOrFail($dto->getId());
+ $attributeOption->update($dto->toArray());
+
+ return $attributeOption;
+ }
+
+ return $this->create($attributeId, $dto);
+ }
+
+ public function delete(AttributeOption $attributeOption): void
+ {
+ $attributeOption->delete();
+ }
+
+ public function deleteAll(string $attributeId): void
+ {
+ AttributeOption::query()
+ ->where('attribute_id', '=', $attributeId)
+ ->delete();
+ }
+}
diff --git a/app/Services/AttributeService.php b/app/Services/AttributeService.php
new file mode 100644
index 000000000..e1e083068
--- /dev/null
+++ b/app/Services/AttributeService.php
@@ -0,0 +1,83 @@
+toArray());
+
+ $this->processAttributeOptions($attribute, $dto);
+
+ return $attribute;
+ }
+
+ public function update(Attribute $attribute, AttributeDto $dto): Attribute
+ {
+ $attribute->update($dto->toArray());
+
+ $this->processAttributeOptions($attribute, $dto);
+
+ return $attribute;
+ }
+
+ public function delete(Attribute $attribute): void
+ {
+ $this->attributeOptionService->deleteAll($attribute->getKey());
+
+ $attribute->delete();
+ }
+
+ public function sync(Product $product, array $data): void
+ {
+ $attributes = Arr::divide($data)[0];
+
+ $product->attributes()->sync($attributes);
+ $product->attributes->each(fn ($attribute) => $attribute->pivot->options()->sync($data[$attribute->getKey()]));
+ }
+
+ public function updateMinMax(Attribute $attribute): void
+ {
+ $attribute->refresh();
+
+ if ($attribute->type->value === AttributeType::NUMBER) {
+ $attribute->min_number = $attribute->options->min('value_number');
+ $attribute->max_number = $attribute->options->max('value_number');
+ }
+
+ if ($attribute->type->value === AttributeType::DATE) {
+ $attribute->min_date = $attribute->options->min('value_date');
+ $attribute->max_date = $attribute->options->max('value_date');
+ }
+
+ $attribute->update();
+ }
+
+ protected function processAttributeOptions(Attribute &$attribute, AttributeDto $dto): Attribute
+ {
+ $attribute->options
+ ->whereNotIn('id', array_map(fn ($option) => $option->getId(), $dto->getOptions()))
+ ->each(
+ fn ($missingOption) => $this->attributeOptionService->delete($missingOption)
+ );
+
+ foreach ($dto->getOptions() as $option) {
+ $this->attributeOptionService->updateOrCreate($attribute->getKey(), $option);
+ }
+
+ return $attribute->refresh();
+ }
+}
diff --git a/app/Services/AvailabilityService.php b/app/Services/AvailabilityService.php
index 1c6504d2b..52b861386 100644
--- a/app/Services/AvailabilityService.php
+++ b/app/Services/AvailabilityService.php
@@ -71,40 +71,37 @@ public function calculateSchemaAvailability(Schema $schema): void
}
public function calculateProductAvailability(Product $product): void
+ {
+ $product->update([
+ 'available' => $this->isProductAvaiable($product),
+ ]);
+ }
+
+ public function isProductAvaiable(Product $product): bool
{
//If every product's item quantity is greater or equal to pivot quantity or product has no schemas
//then product is available
if (
- ($product->items->isNotEmpty()
- && $product->items->every(fn ($item) => $item->pivot->quantity <= $item->quantity))
- || !$product->schemas()->exists()) {
- $product->update([
- 'available' => true,
- ]);
- return;
+ $product->schemas->isEmpty() ||
+ ($product->items->isNotEmpty() &&
+ $product->items->every(fn ($item) => $item->pivot->quantity <= $item->quantity))
+ ) {
+ return true;
}
$requiredSelectSchemas = $product->requiredSchemas->where('type.value', SchemaType::SELECT);
if ($requiredSelectSchemas->isEmpty()) {
- $product->update([
- 'available' => true,
- ]);
-
- return;
+ return true;
}
$hasAvailablePermutations = $this->checkPermutations($requiredSelectSchemas);
if ($hasAvailablePermutations) {
- $product->update([
- 'available' => true,
- ]);
- } else {
- $product->update([
- 'available' => false,
- ]);
+ return true;
}
+
+ return false;
}
public function checkPermutations(Collection $schemas): bool
diff --git a/app/Services/Contracts/AttributeOptionServiceContract.php b/app/Services/Contracts/AttributeOptionServiceContract.php
new file mode 100644
index 000000000..7962b77dc
--- /dev/null
+++ b/app/Services/Contracts/AttributeOptionServiceContract.php
@@ -0,0 +1,17 @@
+reorderService = $reorderService;
}
- public function sync(Product $product, array $media = []): void
+ public function sync(Product $product, array $media): void
{
$operations = $product->media()->sync($this->reorderService->reorder($media));
@@ -37,6 +37,22 @@ public function sync(Product $product, array $media = []): void
}
}
+ public function destroy(Media $media): void
+ {
+ if ($media->products()->exists()) {
+ Gate::authorize('products.edit');
+ }
+
+ $response = Http::withHeaders(['x-api-key' => Config::get('silverbox.key')])
+ ->delete($media->url);
+
+ if ($response->failed()) {
+ throw new MediaCriticalException('CDN responded with an error');
+ }
+
+ $media->forceDelete();
+ }
+
public function store(UploadedFile $file): Media
{
$response = Http::attach('file', $file->getContent(), 'file')
@@ -69,22 +85,6 @@ public function update(Media $media, MediaUpdateDto $dto): Media
return $media;
}
- public function destroy(Media $media): void
- {
- if ($media->products()->exists()) {
- Gate::authorize('products.edit');
- }
-
- $response = Http::withHeaders(['x-api-key' => Config::get('silverbox.key')])
- ->delete($media->url);
-
- if ($response->failed()) {
- throw new MediaCriticalException('CDN responded with an error');
- }
-
- $media->forceDelete();
- }
-
private function getMediaType(string $extension): int
{
return match ($extension) {
diff --git a/app/Services/MetadataService.php b/app/Services/MetadataService.php
new file mode 100644
index 000000000..9f0196048
--- /dev/null
+++ b/app/Services/MetadataService.php
@@ -0,0 +1,54 @@
+updateOrCreate($model, $meta);
+ }
+ }
+
+ public function updateOrCreate(Model|Role $model, MetadataDto $dto): void
+ {
+ $query = $dto->isPublic() ? $model->metadata() : $model->metadataPrivate();
+
+ if ($dto->getValue() === null) {
+ $query->where('name', $dto->getName())->delete();
+
+ return;
+ }
+
+ $query->updateOrCreate(
+ ['name' => $dto->getName()],
+ $dto->toArray(),
+ );
+ }
+
+ public function returnModel(array $routeSegments): Model|Role|null
+ {
+ $segment = Collection::make($routeSegments)->first();
+ $className = 'App\\Models\\' . Str::studly(Str::singular($segment));
+
+ if (class_exists($className)) {
+ return new $className();
+ }
+
+ $className = 'App\\Models\\' . Str::studly($segment);
+
+ if (class_exists($className)) {
+ return new $className();
+ }
+
+ return null;
+ }
+}
diff --git a/app/Services/OrderService.php b/app/Services/OrderService.php
index 33e268f48..e50ba4c13 100644
--- a/app/Services/OrderService.php
+++ b/app/Services/OrderService.php
@@ -91,9 +91,9 @@ public function update(OrderUpdateDto $dto, Order $order): JsonResponse
public function indexUserOrder(OrderIndexDto $dto): LengthAwarePaginator
{
- return Order::search(['user_id' => Auth::id()] + $dto->getSearchCriteria())
+ return Order::searchByCriteria(['user_id' => Auth::id()] + $dto->getSearchCriteria())
->sort($dto->getSort())
- ->with(['products', 'discounts', 'payments'])
+ ->with(['products', 'discounts', 'payments', 'metadata'])
->paginate(Config::get('pagination.per_page'));
}
diff --git a/app/Services/PageService.php b/app/Services/PageService.php
index cf9e315d3..15029fc1e 100644
--- a/app/Services/PageService.php
+++ b/app/Services/PageService.php
@@ -28,9 +28,11 @@ public function authorize(Page $page): void
}
}
- public function getPaginated(): LengthAwarePaginator
+ public function getPaginated(?array $search): LengthAwarePaginator
{
- $query = Page::query()->with('seo');
+ $query = Page::query()
+ ->searchByCriteria($search)
+ ->with(['seo', 'metadata']);
if (!Auth::user()->can('pages.show_hidden')) {
$query->where('public', true);
diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php
index 8ab292da7..d837e9382 100644
--- a/app/Services/PermissionService.php
+++ b/app/Services/PermissionService.php
@@ -11,7 +11,7 @@ class PermissionService implements PermissionServiceContract
public function getAll(?bool $assignable): Collection
{
if ($assignable !== null) {
- return Permission::search([
+ return Permission::searchByCriteria([
'assignable' => $assignable,
])->get();
}
diff --git a/app/Services/ProductSearchService.php b/app/Services/ProductSearchService.php
new file mode 100644
index 000000000..b7567bfc3
--- /dev/null
+++ b/app/Services/ProductSearchService.php
@@ -0,0 +1,118 @@
+ $product->getKey(),
+ 'name' => $product->name,
+ 'slug' => $product->slug,
+ 'hide_on_index' => $this->mapHideOnIndex($product),
+ 'available' => $product->available,
+ 'price' => $product->price,
+ 'price_min' => $product->price_min,
+ 'price_max' => $product->price_max,
+ 'public' => $product->public,
+ 'description' => strip_tags($product->description_html),
+ 'description_short' => $product->description_short,
+ 'created_at' => $product->created_at,
+ 'updated_at' => $product->updated_at,
+ 'order' => $product->order,
+ 'tags' => $product->tags->map(fn (Tag $tag): array => $this->mapTag($tag))->toArray(),
+ 'sets' => $this->mapSets($product),
+ 'attributes' => ProductAttribute::where('product_id', $product->getKey())
+ ->with('options')
+ ->get()
+ ->map(fn (ProductAttribute $attribute): array => $this->mapAttribute($attribute))
+ ->toArray(),
+ 'metadata' => $product->metadata
+ ->map(fn (Metadata $meta): array => $this->mapMeta($meta))
+ ->toArray(),
+ 'metadata_private' => $product->metadataPrivate
+ ->map(fn (Metadata $meta): array => $this->mapMeta($meta))
+ ->toArray(),
+ ];
+ }
+
+ private function mapHideOnIndex(Product $product): bool
+ {
+ $sets = $this->productSetService->flattenParentsSetsTree($product->sets);
+
+ foreach ($sets as $set) {
+ if ($set->hide_on_index) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function mapTag(Tag $tag): array
+ {
+ return [
+ 'id' => $tag->getKey(),
+ 'name' => $tag->name,
+ ];
+ }
+
+ private function mapSets(Product $product): array
+ {
+ $sets = $this->productSetService->flattenParentsSetsTree($product->sets);
+
+ return $sets->map(fn (ProductSet $set): array => $this->mapSet($set))->toArray();
+ }
+
+ private function mapSet(ProductSet $set): array
+ {
+ return [
+ 'id' => $set->getKey(),
+ 'name' => $set->name,
+ 'slug' => $set->slug,
+ 'description' => strip_tags($set->description_html),
+ ];
+ }
+
+ private function mapAttribute(ProductAttribute $attribute): array
+ {
+ return [
+ 'id' => $attribute->attribute->getKey(),
+ 'name' => $attribute->attribute->name,
+ 'slug' => $attribute->attribute->slug,
+ 'type' => $attribute->attribute->type,
+ 'values' => $attribute->options->map(function ($option): array {
+ return [
+ 'id' => $option->getKey(),
+ 'name' => $option->name,
+ 'value_number' => $option->value_number,
+ 'value_date' => $option->value_date,
+ ];
+ })->toArray(),
+ ];
+ }
+
+ private function mapMeta(Metadata $meta): array
+ {
+ return [
+ 'id' => $meta->getKey(),
+ 'name' => $meta->name,
+ 'value' => (string) $meta->value,
+ 'value_type' => $meta->value_type,
+ ];
+ }
+}
diff --git a/app/Services/ProductService.php b/app/Services/ProductService.php
index 2e50fa8a1..7a805e384 100644
--- a/app/Services/ProductService.php
+++ b/app/Services/ProductService.php
@@ -2,41 +2,138 @@
namespace App\Services;
+use App\Dtos\ProductCreateDto;
+use App\Dtos\ProductUpdateDto;
use App\Enums\SchemaType;
+use App\Events\ProductCreated;
+use App\Events\ProductDeleted;
+use App\Events\ProductUpdated;
use App\Models\Option;
use App\Models\Product;
use App\Models\Schema;
+use App\Services\Contracts\AttributeServiceContract;
+use App\Services\Contracts\AvailabilityServiceContract;
+use App\Services\Contracts\MediaServiceContract;
+use App\Services\Contracts\MetadataServiceContract;
use App\Services\Contracts\ProductServiceContract;
+use App\Services\Contracts\SchemaServiceContract;
+use App\Services\Contracts\SeoMetadataServiceContract;
+use Heseya\Dto\Missing;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\DB;
class ProductService implements ProductServiceContract
{
- public function assignItems(Product $product, array|null $itemsIds): Product
+ public function __construct(
+ private MediaServiceContract $mediaService,
+ private SchemaServiceContract $schemaService,
+ private SeoMetadataServiceContract $seoMetadataService,
+ private AvailabilityServiceContract $availabilityService,
+ private MetadataServiceContract $metadataService,
+ private AttributeServiceContract $attributeService,
+ ) {
+ }
+
+ private function setup(Product $product, ProductCreateDto|ProductUpdateDto $dto): Product
{
- if ($itemsIds !== null) {
- $product->items()->detach();
- $items = new Collection($itemsIds);
- $items->each(
- fn ($item) => $product->items()->attach($item['id'], ['quantity' => $item['quantity']])
- );
+ if (!($dto->getSchemas() instanceof Missing)) {
+ $this->schemaService->sync($product, $dto->getSchemas());
+ }
+
+ if (!($dto->getSets() instanceof Missing)) {
+ $product->sets()->sync($dto->getSets());
+ }
+
+ if (!($dto->getItems() instanceof Missing)) {
+ $this->assignItems($product, $dto->getItems());
+ }
+
+ if (!($dto->getMedia() instanceof Missing)) {
+ $this->mediaService->sync($product, $dto->getMedia());
}
- return $product->refresh();
+ if (!($dto->getTags() instanceof Missing)) {
+ $product->tags()->sync($dto->getTags());
+ }
+
+ if (!($dto->getSeo() instanceof Missing)) {
+ $product->seo()->save($this->seoMetadataService->create($dto->getSeo()));
+ }
+
+ if (!($dto->getMetadata() instanceof Missing)) {
+ $this->metadataService->sync($product, $dto->getMetadata());
+ }
+
+ if (!($dto->getAttributes() instanceof Missing)) {
+ $this->attributeService->sync($product, $dto->getAttributes());
+ }
+
+ [$priceMin, $priceMax] = $this->getMinMaxPrices($product);
+ $product->price_min = $priceMin;
+ $product->price_max = $priceMax;
+ $product->available = $this->availabilityService->isProductAvaiable($product);
+
+ return $product;
+ }
+
+ public function create(ProductCreateDto $dto): Product
+ {
+ DB::beginTransaction();
+
+ $product = Product::create($dto->toArray());
+ $product = $this->setup($product, $dto);
+ $product->save();
+
+ DB::commit();
+ ProductCreated::dispatch($product);
+
+ return $product;
}
public function getMinMaxPrices(Product $product): array
{
- $schemaMinMax = $this->getSchemasPrices(
+ [$schemaMin, $schemaMax] = $this->getSchemasPrices(
clone $product->schemas,
clone $product->schemas,
);
return [
- $product->price + $schemaMinMax[0],
- $product->price + $schemaMinMax[1],
+ $product->price + $schemaMin,
+ $product->price + $schemaMax,
];
}
+ public function update(Product $product, ProductUpdateDto $dto): Product
+ {
+ DB::beginTransaction();
+
+ $product->fill($dto->toArray());
+ $product = $this->setup($product, $dto);
+ $product->save();
+
+ DB::commit();
+ ProductUpdated::dispatch($product);
+
+ return $product;
+ }
+
+ public function delete(Product $product): void
+ {
+ ProductDeleted::dispatch($product);
+
+ DB::beginTransaction();
+
+ $this->mediaService->sync($product, []);
+
+ $product->delete();
+
+ if ($product->seo !== null) {
+ $this->seoMetadataService->delete($product->seo);
+ }
+
+ DB::commit();
+ }
+
public function updateMinMaxPrices(Product $product): void
{
$productMinMaxPrices = $this->getMinMaxPrices($product);
@@ -46,6 +143,19 @@ public function updateMinMaxPrices(Product $product): void
]);
}
+ private function assignItems(Product $product, ?array $items): void
+ {
+ $items = Collection::make($items)->mapWithKeys(function (array $item): array {
+ return [
+ $item['id'] => [
+ 'quantity' => $item['quantity'],
+ ],
+ ];
+ });
+
+ $product->items()->sync($items);
+ }
+
private function getSchemasPrices(
Collection $allSchemas,
Collection $remainingSchemas,
diff --git a/app/Services/ProductSetService.php b/app/Services/ProductSetService.php
index 3ad24cf39..ebb27bac1 100644
--- a/app/Services/ProductSetService.php
+++ b/app/Services/ProductSetService.php
@@ -35,7 +35,8 @@ public function authorize(ProductSet $set): void
public function searchAll(array $attributes, bool $root): Collection
{
- $query = ProductSet::search($attributes);
+ $query = ProductSet::searchByCriteria($attributes)
+ ->with('metadata');
if (!Auth::user()->can('product_sets.show_hidden')) {
$query->public();
@@ -75,6 +76,11 @@ public function create(ProductSetDto $dto): ProductSet
'public_parent' => $publicParent,
]);
+ $attributes = Collection::make($dto->getAttributesIds());
+ if ($attributes->isNotEmpty()) {
+ $set->attributes()->sync($attributes);
+ }
+
$children = Collection::make($dto->getChildrenIds());
if ($children->isNotEmpty()) {
$children = $children->map(fn ($id) => ProductSet::findOrFail($id));
@@ -170,6 +176,11 @@ public function update(ProductSet $set, ProductSetDto $dto): ProductSet
'public_parent' => $publicParent,
]);
+ if ($dto->getAttributesIds() !== null) {
+ $attributes = Collection::make($dto->getAttributesIds());
+ $set->attributes()->sync($attributes);
+ }
+
$seo = $set->seo;
if ($seo !== null) {
$this->seoMetadataService->update($dto->getSeo(), $seo);
@@ -234,4 +245,20 @@ public function flattenSetsTree(Collection $sets, string $relation): Collection
return $subsets->flatten()->concat($sets);
}
+
+ /**
+ * Recursive get all parents of set collection if parent exists.
+ */
+ public function flattenParentsSetsTree(Collection $sets): Collection
+ {
+ $subsets = Collection::make();
+
+ foreach ($sets as $set) {
+ if ($set->parent) {
+ $subsets = $subsets->merge($this->flattenParentsSetsTree(Collection::make([$set->parent])));
+ }
+ }
+
+ return $subsets->flatten()->concat($sets);
+ }
}
diff --git a/app/Services/RoleService.php b/app/Services/RoleService.php
index 1e5483ada..0b3dd47b8 100644
--- a/app/Services/RoleService.php
+++ b/app/Services/RoleService.php
@@ -18,7 +18,7 @@ class RoleService implements RoleServiceContract
{
public function search(RoleSearchDto $searchDto, int $limit): LengthAwarePaginator
{
- return Role::search($searchDto->toArray())->paginate($limit);
+ return Role::searchByCriteria($searchDto->toArray())->paginate($limit);
}
public function create(RoleCreateDto $dto): Role
diff --git a/app/Services/ShippingMethodService.php b/app/Services/ShippingMethodService.php
index c3d1e2df0..537705e66 100644
--- a/app/Services/ShippingMethodService.php
+++ b/app/Services/ShippingMethodService.php
@@ -12,9 +12,12 @@
class ShippingMethodService implements ShippingMethodServiceContract
{
- public function index(?string $country, float $cartValue): Collection
+ public function index(?array $search, ?string $country, float $cartValue): Collection
{
- $query = ShippingMethod::query()->orderBy('order');
+ $query = ShippingMethod::query()
+ ->searchByCriteria($search)
+ ->with('metadata')
+ ->orderBy('order');
if (!Auth::user()->can('shipping_methods.show_hidden')) {
$query->where('public', true);
diff --git a/app/Services/SortService.php b/app/Services/SortService.php
new file mode 100644
index 000000000..8dc6331ef
--- /dev/null
+++ b/app/Services/SortService.php
@@ -0,0 +1,44 @@
+sort($query, $sortString, $query->model->getSortable());
+ }
+
+ public function sort(Builder|ScoutBuilder $query, string $sortString, array $sortable): Builder|ScoutBuilder
+ {
+ $sort = explode(',', $sortString);
+
+ foreach ($sort as $option) {
+ $option = explode(':', $option);
+
+ $field = $option[0];
+ Validator::make(
+ $option,
+ [
+ '0' => ['required', 'in:' . implode(',', $sortable)],
+ '1' => ['in:asc,desc'],
+ ],
+ [
+ 'required' => 'You must specify sort field.',
+ '0.in' => "You can't sort by ${field} field.",
+ '1.in' => "Only asc|desc sorting directions are allowed on field ${field}.",
+ ]
+ )->validate();
+
+ $order = count($option) > 1 ? $option[1] : 'asc';
+ $query->orderBy($field, $order);
+ }
+
+ return $query;
+ }
+}
diff --git a/app/Services/UserService.php b/app/Services/UserService.php
index e06339481..78834eeb0 100644
--- a/app/Services/UserService.php
+++ b/app/Services/UserService.php
@@ -19,8 +19,9 @@ class UserService implements UserServiceContract
{
public function index(array $search, ?string $sort, int $limit): LengthAwarePaginator
{
- return User::search($search)
+ return User::searchByCriteria($search)
->sort($sort)
+ ->with('metadata')
->paginate($limit);
}
diff --git a/app/Services/WebHookService.php b/app/Services/WebHookService.php
index fed0504ec..3edd0b34b 100644
--- a/app/Services/WebHookService.php
+++ b/app/Services/WebHookService.php
@@ -11,7 +11,7 @@ class WebHookService implements WebHookServiceContract
{
public function searchAll(array $attributes, ?string $sort): Collection
{
- return WebHook::search($attributes)->sort($sort)->get();
+ return WebHook::searchByCriteria($attributes)->sort($sort)->get();
}
public function create(array $request): WebHook
diff --git a/app/Traits/HasMetadata.php b/app/Traits/HasMetadata.php
new file mode 100644
index 000000000..5b9f1e773
--- /dev/null
+++ b/app/Traits/HasMetadata.php
@@ -0,0 +1,23 @@
+morphMany(Metadata::class, 'model', 'model_type', 'model_id')
+ ->public();
+ }
+
+ public function metadataPrivate(): MorphMany
+ {
+ return $this
+ ->morphMany(Metadata::class, 'model', 'model_type', 'model_id')
+ ->private();
+ }
+}
diff --git a/app/Traits/MetadataResource.php b/app/Traits/MetadataResource.php
new file mode 100644
index 000000000..6b7147f82
--- /dev/null
+++ b/app/Traits/MetadataResource.php
@@ -0,0 +1,32 @@
+processMetadata($this->resource->metadata);
+
+ if ($privateMetadataPermission !== null && Gate::allows($privateMetadataPermission)) {
+ $data['metadata_private'] = $this->processMetadata($this->resource->metadataPrivate);
+ }
+
+ return $data;
+ }
+
+ private function processMetadata(Collection $data)
+ {
+ /**
+ * Special workaround for frond-end requirements
+ * */
+ if ($data->count() <= 0) {
+ return (object) [];
+ }
+
+ return $data->mapWithKeys(fn ($meta) => [$meta->name => $meta->value]);
+ }
+}
diff --git a/app/Traits/MetadataRules.php b/app/Traits/MetadataRules.php
new file mode 100644
index 000000000..52ccb8e54
--- /dev/null
+++ b/app/Traits/MetadataRules.php
@@ -0,0 +1,14 @@
+ ['array'],
+ 'metadata_private' => ['array'],
+ ];
+ }
+}
diff --git a/app/Traits/PermissionUtility.php b/app/Traits/PermissionUtility.php
new file mode 100644
index 000000000..3bef59ef2
--- /dev/null
+++ b/app/Traits/PermissionUtility.php
@@ -0,0 +1,34 @@
+getPermissionPrefix($model)}.${ability}");
+ }
+
+ protected function deniesAbilityByModel(string $ability, $model): bool
+ {
+ return !$this->allowsAbilityByModel($ability, $model);
+ }
+
+ /**
+ * Allows to change returned prefix to desired model if is different to table name
+ *
+ * @param $model
+ *
+ * @return string
+ * */
+ protected function getPermissionPrefix($model): string
+ {
+ return match ($model::class) {
+ PackageTemplate::class => 'packages',
+ default => $model->getTable(),
+ };
+ }
+}
diff --git a/app/Traits/SeoMetadataRules.php b/app/Traits/SeoMetadataRules.php
deleted file mode 100644
index f34c9ae41..000000000
--- a/app/Traits/SeoMetadataRules.php
+++ /dev/null
@@ -1,21 +0,0 @@
- ['nullable', 'string', 'max:255'],
- 'seo.description' => ['nullable', 'string', 'max:1000'],
- 'seo.keywords' => ['nullable', 'array'],
- 'seo.og_image_id' => ['nullable', 'uuid', 'exists:media,id'],
- 'seo.twitter_card' => ['nullable', new EnumValue(TwitterCardType::class, false)],
- 'seo.no_index' => ['nullable', 'boolean'],
- ];
- }
-}
diff --git a/app/Traits/SeoRules.php b/app/Traits/SeoRules.php
new file mode 100644
index 000000000..77fb3352f
--- /dev/null
+++ b/app/Traits/SeoRules.php
@@ -0,0 +1,21 @@
+ ['nullable', 'string', 'max:255'],
+ "{$prefix}description" => ['nullable', 'string', 'max:1000'],
+ "{$prefix}keywords" => ['nullable', 'array'],
+ "{$prefix}og_image_id" => ['nullable', 'uuid', 'exists:media,id'],
+ "{$prefix}twitter_card" => ['nullable', new EnumValue(TwitterCardType::class, false)],
+ "{$prefix}no_index" => ['nullable', 'boolean'],
+ ];
+ }
+}
diff --git a/app/Traits/Sortable.php b/app/Traits/Sortable.php
new file mode 100644
index 000000000..b663cabfc
--- /dev/null
+++ b/app/Traits/Sortable.php
@@ -0,0 +1,40 @@
+sort($query, $sortString, $this->getSortable());
+ }
+
+ return $query->orderBy(
+ $this->getDefaultSortBy(),
+ $this->getDefaultSortDirection(),
+ );
+ }
+
+ public function getSortable(): array
+ {
+ // @phpstan-ignore-next-line
+ return $this->sortable ?? [];
+ }
+
+ public function getDefaultSortBy(): string
+ {
+ // @phpstan-ignore-next-line
+ return $this->defaultSortBy ?? 'created_at';
+ }
+
+ public function getDefaultSortDirection(): string
+ {
+ // @phpstan-ignore-next-line
+ return $this->defaultSortDirection ?? 'asc';
+ }
+}
diff --git a/composer.json b/composer.json
index 23878f2f8..728be2055 100644
--- a/composer.json
+++ b/composer.json
@@ -10,11 +10,12 @@
"guzzlehttp/guzzle": "^7.4",
"heseya/2fa": "^2.0",
"heseya/dto": "^1.0",
- "heseya/laravel-searchable": "^1.0",
- "heseya/pagination": "^1.0",
+ "heseya/laravel-searchable": "^2.0",
+ "heseya/pagination": "^1.0.2",
"heseya/resource": "^1.0",
- "heseya/sortable": "^1.0",
+ "bvlinsky/explorer": "^3.0",
"laravel/framework": "^9.4",
+ "laravel/scout": "^9.4.5",
"league/html-to-markdown": "^5.1",
"league/omnipay": "^3.2",
"omnipay/common": "^3.2",
@@ -111,14 +112,6 @@
{
"type": "path",
"url": "./heseya/resource"
- },
- {
- "type": "path",
- "url": "./heseya/pagination"
- },
- {
- "type": "path",
- "url": "./heseya/sortable"
}
]
}
diff --git a/composer.lock b/composer.lock
index 0df992727..bea60a23f 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "b4becf595e9e34f9bbe5b542326328dd",
+ "content-hash": "76746346c63e2e8bf9e8e96d9aff4263",
"packages": [
{
"name": "bensampo/laravel-enum",
@@ -150,6 +150,72 @@
],
"time": "2021-08-15T20:50:18+00:00"
},
+ {
+ "name": "bvlinsky/explorer",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/bvlinsky/Explorer.git",
+ "reference": "72333facc97df1b1ea584ea073d92d0835873b90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/bvlinsky/Explorer/zipball/72333facc97df1b1ea584ea073d92d0835873b90",
+ "reference": "72333facc97df1b1ea584ea073d92d0835873b90",
+ "shasum": ""
+ },
+ "require": {
+ "elasticsearch/elasticsearch": "^7.16",
+ "illuminate/support": "^9.0",
+ "laravel/scout": "^9.0",
+ "php": "8.0.*||8.1.*",
+ "webmozart/assert": "^1.10"
+ },
+ "require-dev": {
+ "infection/infection": "^0.26",
+ "mockery/mockery": "^1.4",
+ "phpunit/phpunit": "~9.0",
+ "symplify/easy-coding-standard": "^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "JeroenG\\Explorer\\ExplorerServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "JeroenG\\Explorer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "EUPL-1.2"
+ ],
+ "authors": [
+ {
+ "name": "Jeroen",
+ "email": "jeroengjeroeng@gmail.com",
+ "homepage": "https://jeroeng.dev"
+ }
+ ],
+ "description": "Next-gen Elasticsearch driver for Laravel Scout.",
+ "homepage": "https://jeroen-g.github.io/Explorer/",
+ "keywords": [
+ "elastic",
+ "elasticsearch",
+ "explorer",
+ "laravel",
+ "scout",
+ "search"
+ ],
+ "support": {
+ "source": "https://github.com/bvlinsky/Explorer/tree/3.0.0"
+ },
+ "time": "2022-03-26T15:24:54+00:00"
+ },
{
"name": "clue/stream-filter",
"version": "v1.6.0",
@@ -1482,6 +1548,183 @@
],
"time": "2021-10-11T09:18:27+00:00"
},
+ {
+ "name": "elasticsearch/elasticsearch",
+ "version": "v7.17.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/elastic/elasticsearch-php.git",
+ "reference": "1890f9d7fde076b5a3ddcf579a802af05b2e781b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/1890f9d7fde076b5a3ddcf579a802af05b2e781b",
+ "reference": "1890f9d7fde076b5a3ddcf579a802af05b2e781b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": ">=1.3.7",
+ "ezimuel/ringphp": "^1.1.2",
+ "php": "^7.3 || ^8.0",
+ "psr/log": "^1|^2|^3"
+ },
+ "require-dev": {
+ "ext-yaml": "*",
+ "ext-zip": "*",
+ "mockery/mockery": "^1.2",
+ "phpstan/phpstan": "^0.12",
+ "phpunit/phpunit": "^9.3",
+ "squizlabs/php_codesniffer": "^3.4",
+ "symfony/finder": "~4.0"
+ },
+ "suggest": {
+ "ext-curl": "*",
+ "monolog/monolog": "Allows for client-level logging and tracing"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/autoload.php"
+ ],
+ "psr-4": {
+ "Elasticsearch\\": "src/Elasticsearch/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0",
+ "LGPL-2.1-only"
+ ],
+ "authors": [
+ {
+ "name": "Zachary Tong"
+ },
+ {
+ "name": "Enrico Zimuel"
+ }
+ ],
+ "description": "PHP Client for Elasticsearch",
+ "keywords": [
+ "client",
+ "elasticsearch",
+ "search"
+ ],
+ "support": {
+ "issues": "https://github.com/elastic/elasticsearch-php/issues",
+ "source": "https://github.com/elastic/elasticsearch-php/tree/v7.17.0"
+ },
+ "time": "2022-02-03T13:40:04+00:00"
+ },
+ {
+ "name": "ezimuel/guzzlestreams",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ezimuel/guzzlestreams.git",
+ "reference": "abe3791d231167f14eb80d413420d1eab91163a8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ezimuel/guzzlestreams/zipball/abe3791d231167f14eb80d413420d1eab91163a8",
+ "reference": "abe3791d231167f14eb80d413420d1eab91163a8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Stream\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ }
+ ],
+ "description": "Fork of guzzle/streams (abandoned) to be used with elasticsearch-php",
+ "homepage": "http://guzzlephp.org/",
+ "keywords": [
+ "Guzzle",
+ "stream"
+ ],
+ "support": {
+ "source": "https://github.com/ezimuel/guzzlestreams/tree/3.0.1"
+ },
+ "time": "2020-02-14T23:11:50+00:00"
+ },
+ {
+ "name": "ezimuel/ringphp",
+ "version": "1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ezimuel/ringphp.git",
+ "reference": "92b8161404ab1ad84059ebed41d9f757e897ce74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ezimuel/ringphp/zipball/92b8161404ab1ad84059ebed41d9f757e897ce74",
+ "reference": "92b8161404ab1ad84059ebed41d9f757e897ce74",
+ "shasum": ""
+ },
+ "require": {
+ "ezimuel/guzzlestreams": "^3.0.1",
+ "php": ">=5.4.0",
+ "react/promise": "~2.0"
+ },
+ "replace": {
+ "guzzlehttp/ringphp": "self.version"
+ },
+ "require-dev": {
+ "ext-curl": "*",
+ "phpunit/phpunit": "~9.0"
+ },
+ "suggest": {
+ "ext-curl": "Guzzle will use specific adapters if cURL is present"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Ring\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ }
+ ],
+ "description": "Fork of guzzle/RingPHP (abandoned) to be used with elasticsearch-php",
+ "support": {
+ "source": "https://github.com/ezimuel/ringphp/tree/1.2.0"
+ },
+ "time": "2021-11-16T11:51:30+00:00"
+ },
{
"name": "fruitcake/php-cors",
"version": "v1.2.0",
@@ -2153,16 +2396,16 @@
},
{
"name": "heseya/laravel-searchable",
- "version": "1.0.0",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/heseya/laravel-searchable.git",
- "reference": "301235f4e67784af659f7e81da19b674fc4ed525"
+ "reference": "6daaaf153fc9ffdb19bed8252b150126feac9f0f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/heseya/laravel-searchable/zipball/301235f4e67784af659f7e81da19b674fc4ed525",
- "reference": "301235f4e67784af659f7e81da19b674fc4ed525",
+ "url": "https://api.github.com/repos/heseya/laravel-searchable/zipball/6daaaf153fc9ffdb19bed8252b150126feac9f0f",
+ "reference": "6daaaf153fc9ffdb19bed8252b150126feac9f0f",
"shasum": ""
},
"require": {
@@ -2170,7 +2413,7 @@
"php": ">=7.3"
},
"require-dev": {
- "pestphp/pest": "^0.2.4"
+ "pestphp/pest": "^1.21"
},
"type": "library",
"autoload": {
@@ -2190,20 +2433,27 @@
"description": "Search trait for Eloquent models",
"support": {
"issues": "https://github.com/heseya/laravel-searchable/issues",
- "source": "https://github.com/heseya/laravel-searchable/tree/master"
+ "source": "https://github.com/heseya/laravel-searchable/tree/2.0.0"
},
- "time": "2020-08-14T08:39:11+00:00"
+ "time": "2022-03-14T10:36:47+00:00"
},
{
"name": "heseya/pagination",
- "version": "1.0.0",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/heseya/laravel-pagination.git",
+ "reference": "ac83ba6801f213e3b61126ccf4cf66dd09c41a07"
+ },
"dist": {
- "type": "path",
- "url": "./heseya/pagination",
- "reference": "b19285987b8964ee46414770f789e45f9749abbd"
+ "type": "zip",
+ "url": "https://api.github.com/repos/heseya/laravel-pagination/zipball/ac83ba6801f213e3b61126ccf4cf66dd09c41a07",
+ "reference": "ac83ba6801f213e3b61126ccf4cf66dd09c41a07",
+ "shasum": ""
},
"require": {
- "laravel/framework": "^9.0"
+ "laravel/framework": "^8.0|^9.0",
+ "php": "^8.0"
},
"type": "library",
"extra": {
@@ -2215,20 +2465,25 @@
},
"autoload": {
"psr-4": {
- "Heseya\\Pagination\\": "src/",
- "Heseya\\Pagination\\Tests\\": "tests/"
+ "Heseya\\Pagination\\": "src/"
}
},
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
"authors": [
{
"name": "Jędrzej Buliński",
"email": "jedrzej@heseya.com"
}
],
- "description": "Heseya Store Pagination Middleware",
- "transport-options": {
- "relative": true
- }
+ "description": "Heseya Laravel Pagination Middleware",
+ "support": {
+ "issues": "https://github.com/heseya/laravel-pagination/issues",
+ "source": "https://github.com/heseya/laravel-pagination/tree/1.0.2"
+ },
+ "time": "2022-03-29T06:19:39+00:00"
},
{
"name": "heseya/resource",
@@ -2255,31 +2510,6 @@
"relative": true
}
},
- {
- "name": "heseya/sortable",
- "version": "1.0.0",
- "dist": {
- "type": "path",
- "url": "./heseya/sortable",
- "reference": "4183c93f3b2c36c0f2b429c0d78ce31c4fbd984c"
- },
- "require": {
- "laravel/framework": "^9.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Heseya\\Sortable\\": "src/"
- }
- },
- "license": [
- "MIT"
- ],
- "description": "Sortable Trait for Laravel",
- "transport-options": {
- "relative": true
- }
- },
{
"name": "http-interop/http-factory-guzzle",
"version": "1.2.0",
@@ -2712,6 +2942,78 @@
},
"time": "2022-03-29T14:41:26+00:00"
},
+ {
+ "name": "laravel/scout",
+ "version": "v9.4.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/scout.git",
+ "reference": "c4b697218ea7abe89894312caf86668b1f169233"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/scout/zipball/c4b697218ea7abe89894312caf86668b1f169233",
+ "reference": "c4b697218ea7abe89894312caf86668b1f169233",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/bus": "^8.0|^9.0",
+ "illuminate/contracts": "^8.0|^9.0",
+ "illuminate/database": "^8.0|^9.0",
+ "illuminate/http": "^8.0|^9.0",
+ "illuminate/pagination": "^8.0|^9.0",
+ "illuminate/queue": "^8.0|^9.0",
+ "illuminate/support": "^8.0|^9.0",
+ "php": "^7.3|^8.0"
+ },
+ "require-dev": {
+ "meilisearch/meilisearch-php": "^0.19",
+ "mockery/mockery": "^1.0",
+ "orchestra/testbench": "^6.17|^7.0",
+ "phpunit/phpunit": "^9.3"
+ },
+ "suggest": {
+ "algolia/algoliasearch-client-php": "Required to use the Algolia engine (^3.2).",
+ "meilisearch/meilisearch-php": "Required to use the MeiliSearch engine (^0.23)."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "9.x-dev"
+ },
+ "laravel": {
+ "providers": [
+ "Laravel\\Scout\\ScoutServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Scout\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "Laravel Scout provides a driver based solution to searching your Eloquent models.",
+ "keywords": [
+ "algolia",
+ "laravel",
+ "search"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/scout/issues",
+ "source": "https://github.com/laravel/scout"
+ },
+ "time": "2022-03-29T15:56:47+00:00"
+ },
{
"name": "laravel/serializable-closure",
"version": "v1.1.1",
@@ -11344,16 +11646,16 @@
},
{
"name": "phpstan/phpstan",
- "version": "1.5.3",
+ "version": "1.5.4",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
- "reference": "39953ac1452a8843702ee41a35b4861d3e8207a7"
+ "reference": "bbf68cae24f6dc023c607ea0f87da55dd9d55c2b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/39953ac1452a8843702ee41a35b4861d3e8207a7",
- "reference": "39953ac1452a8843702ee41a35b4861d3e8207a7",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/bbf68cae24f6dc023c607ea0f87da55dd9d55c2b",
+ "reference": "bbf68cae24f6dc023c607ea0f87da55dd9d55c2b",
"shasum": ""
},
"require": {
@@ -11379,7 +11681,7 @@
"description": "PHPStan - PHP Static Analysis Tool",
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
- "source": "https://github.com/phpstan/phpstan/tree/1.5.3"
+ "source": "https://github.com/phpstan/phpstan/tree/1.5.4"
},
"funding": [
{
@@ -11399,7 +11701,7 @@
"type": "tidelift"
}
],
- "time": "2022-03-30T21:55:08+00:00"
+ "time": "2022-04-03T12:39:00+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -12266,16 +12568,16 @@
},
{
"name": "sebastian/environment",
- "version": "5.1.3",
+ "version": "5.1.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "388b6ced16caa751030f6a69e588299fa09200ac"
+ "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac",
- "reference": "388b6ced16caa751030f6a69e588299fa09200ac",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7",
+ "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7",
"shasum": ""
},
"require": {
@@ -12317,7 +12619,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
- "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3"
+ "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4"
},
"funding": [
{
@@ -12325,7 +12627,7 @@
"type": "github"
}
],
- "time": "2020-09-28T05:52:38+00:00"
+ "time": "2022-04-03T09:37:03+00:00"
},
{
"name": "sebastian/exporter",
diff --git a/config/app.php b/config/app.php
index b725d3944..0dd6a8f24 100644
--- a/config/app.php
+++ b/config/app.php
@@ -15,14 +15,6 @@
'name' => env('APP_NAME', 'Heseya Store'),
- /*
- |--------------------------------------------------------------------------
- | Application Version
- |--------------------------------------------------------------------------
- */
-
- 'ver' => '2.0.5',
-
/*
|--------------------------------------------------------------------------
| Application Environment
@@ -180,6 +172,7 @@
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
+ App\Providers\RepositoryServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
diff --git a/config/explorer.php b/config/explorer.php
new file mode 100644
index 000000000..b3a9136e1
--- /dev/null
+++ b/config/explorer.php
@@ -0,0 +1,31 @@
+ [
+ 'host' => Env::get('ELASTICSEARCH_HOST', 'elasticsearch'),
+ ],
+
+ /**
+ * An index may be defined on an Eloquent model or inline below. A more in depth explanation
+ * of the mapping possibilities can be found in the documentation of Explorer's repository.
+ */
+ 'indexes' => [
+ Product::class,
+ ],
+
+ /**
+ * You may opt to keep the old indices after the alias is pointed to a new index.
+ * A model is only using index aliases if it implements the Aliased interface.
+ */
+ 'prune_old_aliases' => true,
+];
diff --git a/config/insights.php b/config/insights.php
index 845b59218..c50a1e5d0 100644
--- a/config/insights.php
+++ b/config/insights.php
@@ -3,6 +3,9 @@
declare(strict_types=1);
use Heseya\Insights\Sniffs\NotSpaceAfterNot;
+use NunoMaduro\PhpInsights\Domain\Insights\Composer\ComposerMustBeValid;
+use NunoMaduro\PhpInsights\Domain\Insights\ForbiddenDefineFunctions;
+use NunoMaduro\PhpInsights\Domain\Insights\ForbiddenGlobals;
use NunoMaduro\PhpInsights\Domain\Insights\ForbiddenNormalClasses;
use NunoMaduro\PhpInsights\Domain\Insights\ForbiddenPrivateMethods;
use NunoMaduro\PhpInsights\Domain\Insights\ForbiddenTraits;
@@ -13,6 +16,7 @@
use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineEndingsSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\Formatting\SpaceAfterNotSniff;
+use SlevomatCodingStandard\Sniffs\Arrays\DisallowImplicitArrayCreationSniff;
use PHP_CodeSniffer\Standards\Generic\Sniffs\PHP\ForbiddenFunctionsSniff;
use PhpCsFixer\Fixer\FunctionNotation\VoidReturnFixer;
use SlevomatCodingStandard\Sniffs\Classes\ForbiddenPublicPropertySniff;
@@ -81,6 +85,8 @@
// replaced with own
SpaceAfterNotSniff::class,
DisallowShortTernaryOperatorSniff::class,
+ ForbiddenGlobals::class,
+ DisallowImplicitArrayCreationSniff::class,
],
'config' => [
diff --git a/config/pagination.php b/config/pagination.php
new file mode 100644
index 000000000..eb2fcfec9
--- /dev/null
+++ b/config/pagination.php
@@ -0,0 +1,39 @@
+ Env::get('PAGINATION_LIMIT_KEY', 'limit'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Pagination default
+ |--------------------------------------------------------------------------
+ |
+ | Default pagination limit, when no pagination was found in request.
+ |
+ */
+ 'per_page' => (int) Env::get('PAGINATION_DEFAULT', 24),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Pagination max
+ |--------------------------------------------------------------------------
+ |
+ | Max pagination limit.
+ |
+ */
+ 'max' => (int) Env::get('PAGINATION_MAX', 500),
+
+];
diff --git a/config/scout.php b/config/scout.php
new file mode 100644
index 000000000..429e9975c
--- /dev/null
+++ b/config/scout.php
@@ -0,0 +1,101 @@
+ env('SCOUT_DRIVER', 'elastic'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Index Prefix
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify a prefix that will be applied to all search index
+ | names used by Scout. This prefix may be useful if you have multiple
+ | "tenants" or applications sharing the same search infrastructure.
+ |
+ */
+
+ 'prefix' => env('SCOUT_PREFIX', ''),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Queue Data Syncing
+ |--------------------------------------------------------------------------
+ |
+ | This option allows you to control if the operations that sync your data
+ | with your search engines are queued. When this is set to "true" then
+ | all automatic data syncing will get queued for better performance.
+ |
+ */
+
+ 'queue' => env('SCOUT_QUEUE', false),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Database Transactions
+ |--------------------------------------------------------------------------
+ |
+ | This configuration option determines if your data will only be synced
+ | with your search indexes after every open database transaction has
+ | been committed, thus preventing any discarded data from syncing.
+ |
+ */
+
+ 'after_commit' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Chunk Sizes
+ |--------------------------------------------------------------------------
+ |
+ | These options allow you to control the maximum chunk size when you are
+ | mass importing data into the search engine. This allows you to fine
+ | tune each of these chunk sizes based on the power of the servers.
+ |
+ */
+
+ 'chunk' => [
+ 'searchable' => 500,
+ 'unsearchable' => 500,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Soft Deletes
+ |--------------------------------------------------------------------------
+ |
+ | This option allows to control whether to keep soft deleted records in
+ | the search indexes. Maintaining soft deleted records can be useful
+ | if your application still needs to search for the records later.
+ |
+ */
+
+ 'soft_delete' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Identify User
+ |--------------------------------------------------------------------------
+ |
+ | This option allows you to control whether to notify the search engine
+ | of the user performing the search. This is sometimes useful if the
+ | engine supports any analytics based on this application's users.
+ |
+ | Supported engines: "algolia"
+ |
+ */
+
+ 'identify' => env('SCOUT_IDENTIFY', false),
+
+];
diff --git a/config/search.php b/config/search.php
new file mode 100644
index 000000000..94616c5a0
--- /dev/null
+++ b/config/search.php
@@ -0,0 +1,84 @@
+ [
+ 'stopwords' => [
+ 'a',
+ 'aby',
+ 'ach',
+ 'acz',
+ 'aczkolwiek',
+ 'aj',
+ 'albo',
+ 'ale',
+ 'ależ',
+ 'ani',
+ 'aż',
+ 'bardziej',
+ 'bardzo',
+ 'bo',
+ 'bowiem',
+ 'by',
+ 'byli',
+ 'bynajmniej',
+ 'być',
+ 'był',
+ 'była',
+ 'było',
+ 'były',
+ 'będzie',
+ 'będą',
+ 'cali',
+ 'cała',
+ 'cały',
+ 'ci',
+ 'cię',
+ 'ciebie',
+ 'co',
+ 'cokolwiek',
+ 'coś',
+ 'czasami',
+ 'czasem',
+ 'czemu',
+ 'czy',
+ 'czyli',
+ 'daleko',
+ 'dla',
+ 'dlaczego',
+ 'dlatego',
+ 'do',
+ 'dobrze',
+ 'dokąd',
+ 'dość',
+ 'dużo',
+ 'dwa',
+ 'dwaj',
+ 'dwie',
+ 'dwoje',
+ 'dziś',
+ 'dzisiaj',
+ 'gdy',
+ 'gdyby',
+ 'gdyż',
+ 'gdzie',
+ 'gdziekolwiek',
+ 'gdzieś',
+ 'go',
+ 'i',
+ 'ich',
+ 'ile',
+ 'im',
+ 'inna',
+ 'inne',
+ 'inny',
+ 'innych',
+ 'iż',
+ 'ja',
+ 'ją',
+ 'jak',
+ 'jakaś',
+ ],
+ ],
+
+];
diff --git a/database/factories/AttributeFactory.php b/database/factories/AttributeFactory.php
new file mode 100644
index 000000000..46d70c831
--- /dev/null
+++ b/database/factories/AttributeFactory.php
@@ -0,0 +1,37 @@
+faker->unique()->word;
+
+ return [
+ 'name' => $name,
+ 'slug' => Str::slug($name),
+ 'description' => $this->faker->sentence,
+ 'type' => AttributeType::getRandomValue(),
+ 'global' => $this->faker->boolean,
+ 'sortable' => $this->faker->boolean,
+ ];
+ }
+}
diff --git a/database/factories/AttributeOptionFactory.php b/database/factories/AttributeOptionFactory.php
new file mode 100644
index 000000000..74c24d0d8
--- /dev/null
+++ b/database/factories/AttributeOptionFactory.php
@@ -0,0 +1,30 @@
+ $this->faker->word,
+ 'value_number' => rand(0, 1) === 1 ? $this->faker->randomNumber(5) : null,
+ 'value_date' => $this->faker->date,
+ ];
+ }
+}
diff --git a/database/factories/MetadataFactory.php b/database/factories/MetadataFactory.php
new file mode 100644
index 000000000..054f340fa
--- /dev/null
+++ b/database/factories/MetadataFactory.php
@@ -0,0 +1,30 @@
+ $this->faker->word,
+ 'value' => $this->faker->word,
+ 'value_type' => MetadataType::getRandomValue(),
+ 'public' => $this->faker->boolean,
+ ];
+ }
+}
diff --git a/database/factories/RoleFactory.php b/database/factories/RoleFactory.php
index 4e1b8af59..e6ce8d24f 100644
--- a/database/factories/RoleFactory.php
+++ b/database/factories/RoleFactory.php
@@ -20,7 +20,7 @@ class RoleFactory extends Factory
public function definition(): array
{
return [
- 'name' => $this->faker->word,
+ 'name' => $this->faker->word . '-' . rand(1, 99999),
'description' => $this->faker->sentence(10),
];
}
diff --git a/database/migrations/2021_11_26_094412_precalculate_product_visibility.php b/database/migrations/2021_11_26_094412_precalculate_product_visibility.php
index 4c716fa4f..29917175f 100644
--- a/database/migrations/2021_11_26_094412_precalculate_product_visibility.php
+++ b/database/migrations/2021_11_26_094412_precalculate_product_visibility.php
@@ -1,6 +1,7 @@
boolean('public_legacy')->nullable(); // Deprecated, to be removed in 3.0
});
- Product::chunk(100, fn ($products) => $products->each(
+ Product::chunk(100, fn (Collection $products) => $products->each(
function (Product $product) {
$isAnySetPublic = $product->sets->count() === 0 ||
$product->sets->where('public', true)->where('public_parent', true);
$newPublic = $product->public && $isAnySetPublic;
- $product->update([
- 'public_legacy' => $product->public,
- 'public' => $newPublic,
- ]);
+ Product::withoutSyncingToSearch(function () use ($product, $newPublic): void {
+ $product->update([
+ 'public_legacy' => $product->public,
+ 'public' => $newPublic,
+ ]);
+ });
},
));
}
@@ -40,7 +43,7 @@ function (Product $product) {
*/
public function down()
{
- Product::chunk(100, fn ($products) => $products->each(
+ Product::chunk(100, fn (Collection $products) => $products->each(
fn (Product $product) => $product->update([
'public' => $product->public_legacy,
]),
diff --git a/database/migrations/2022_02_09_133640_create_attributes_table.php b/database/migrations/2022_02_09_133640_create_attributes_table.php
new file mode 100644
index 000000000..a40d0d134
--- /dev/null
+++ b/database/migrations/2022_02_09_133640_create_attributes_table.php
@@ -0,0 +1,41 @@
+uuid('id')->primary();
+ $table->string('name');
+ $table->string('slug')->unique()->index();
+ $table->string('description')->nullable();
+ $table->float('min_number')->nullable();
+ $table->float('max_number')->nullable();
+ $table->date('min_date')->nullable();
+ $table->date('max_date')->nullable();
+ $table->string('type');
+ $table->boolean('global');
+ $table->boolean('sortable');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('attributes');
+ }
+}
diff --git a/database/migrations/2022_02_09_133727_create_attribute_options_table.php b/database/migrations/2022_02_09_133727_create_attribute_options_table.php
new file mode 100644
index 000000000..06d23f81e
--- /dev/null
+++ b/database/migrations/2022_02_09_133727_create_attribute_options_table.php
@@ -0,0 +1,37 @@
+uuid('id')->primary();
+ $table->string('name')->nullable();
+ $table->integer('index');
+ $table->float('value_number')->nullable()->default(null);
+ $table->date('value_date')->nullable()->default(null);
+ $table->foreignUuid('attribute_id')->references('id')->on('attributes')->onDelete('cascade')->nullable();
+ $table->timestamps();
+ $table->softDeletes();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('attribute_options');
+ }
+}
diff --git a/database/migrations/2022_02_10_135355_attribute_permissions.php b/database/migrations/2022_02_10_135355_attribute_permissions.php
new file mode 100644
index 000000000..d5e5d7c22
--- /dev/null
+++ b/database/migrations/2022_02_10_135355_attribute_permissions.php
@@ -0,0 +1,71 @@
+ 'attributes.show', 'display_name' => 'Dostęp do listy cech']);
+ Permission::create(['name' => 'attributes.add', 'display_name' => 'Możliwość tworzenia cech']);
+ Permission::create([
+ 'name' => 'attributes.edit',
+ 'display_name' => 'Możliwość edycji cech oraz modyfikacji ich opcji',
+ 'description' => 'Pozwala również na dodawanie nowych i usuwanie opcji cech'
+ ]);
+ Permission::create(['name' => 'attributes.remove', 'display_name' => 'Możliwość usuwania cech']);
+
+ $owner = Role::query()->where('type', '=', RoleType::OWNER)->firstOrFail();
+ $owner->givePermissionTo([
+ 'attributes.show',
+ 'attributes.add',
+ 'attributes.edit',
+ 'attributes.remove',
+ ]);
+ $owner->save();
+
+ $authenticated = Role::query()->where('type', '=', RoleType::AUTHENTICATED)->firstOrFail();
+ $authenticated->givePermissionTo([
+ 'attributes.show',
+ ]);
+ $authenticated->save();
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ $owner = Role::query()->where('type', '=', RoleType::OWNER)->firstOrFail();
+ $owner->revokePermissionTo([
+ 'attributes.show',
+ 'attributes.add',
+ 'attributes.edit',
+ 'attributes.remove',
+ ]);
+ $owner->save();
+
+ $authenticated = Role::query()->where('type', '=', RoleType::AUTHENTICATED)->firstOrFail();
+ $authenticated->revokePermissionTo([
+ 'attributes.show',
+ ]);
+ $authenticated->save();
+
+ Permission::findByName('attributes.show')->delete();
+ Permission::findByName('attributes.add')->delete();
+ Permission::findByName('attributes.edit')->delete();
+ Permission::findByName('attributes.remove')->delete();
+ }
+}
diff --git a/database/migrations/2022_02_15_113642_product_attribute.php b/database/migrations/2022_02_15_113642_product_attribute.php
new file mode 100644
index 000000000..27edd2f42
--- /dev/null
+++ b/database/migrations/2022_02_15_113642_product_attribute.php
@@ -0,0 +1,48 @@
+foreignUuid('product_id')
+ ->index()
+ ->references('id')
+ ->on('products')
+ ->onDelete('cascade');
+
+ $table->foreignUuid('attribute_id')
+ ->index()
+ ->references('id')
+ ->on('attributes')
+ ->onDelete('cascade');
+
+ $table->foreignUuid('option_id')
+ ->index()
+ ->references('id')
+ ->on('attribute_options')
+ ->onDelete('cascade');
+
+ $table->primary(['product_id', 'attribute_id']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('product_attribute');
+ }
+}
diff --git a/database/migrations/2022_02_22_090707_create_attribute_product_set_table.php b/database/migrations/2022_02_22_090707_create_attribute_product_set_table.php
new file mode 100644
index 000000000..ad67ad5b3
--- /dev/null
+++ b/database/migrations/2022_02_22_090707_create_attribute_product_set_table.php
@@ -0,0 +1,31 @@
+uuid('attribute_id')->index();
+ $table->uuid('product_set_id')->index();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('attribute_product_set');
+ }
+}
diff --git a/database/migrations/2022_03_01_073057_add_available_column_to_options_schemas_and_products.php b/database/migrations/2022_03_01_073057_add_available_column_to_options_schemas_and_products.php
index ff667aa87..cc8ebb8c4 100644
--- a/database/migrations/2022_03_01_073057_add_available_column_to_options_schemas_and_products.php
+++ b/database/migrations/2022_03_01_073057_add_available_column_to_options_schemas_and_products.php
@@ -34,7 +34,11 @@ public function up()
$items->each(fn ($item) => $availabilityService->calculateAvailabilityOnOrderAndRestock($item));
$products = Product::doesntHave('schemas')->get();
- $products->each(fn ($product) => $product->update(['available' => true]));
+ $products->each(function (Product $product): void {
+ Product::withoutSyncingToSearch(function () use ($product): void {
+ $product->update(['available' => true]);
+ });
+ });
}
/**
diff --git a/database/migrations/2022_03_03_095726_create_metadata_table.php b/database/migrations/2022_03_03_095726_create_metadata_table.php
new file mode 100644
index 000000000..605708ab8
--- /dev/null
+++ b/database/migrations/2022_03_03_095726_create_metadata_table.php
@@ -0,0 +1,36 @@
+uuid('id')->primary();
+ $table->string('name');
+ $table->string('value');
+ $table->string('value_type');
+ $table->uuidMorphs('model');
+ $table->boolean('public')->default(false);
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('metadata');
+ }
+}
diff --git a/database/migrations/2022_03_03_103126_add_metadata_permissions.php b/database/migrations/2022_03_03_103126_add_metadata_permissions.php
new file mode 100644
index 000000000..79c3d6d67
--- /dev/null
+++ b/database/migrations/2022_03_03_103126_add_metadata_permissions.php
@@ -0,0 +1,48 @@
+ 'orders.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych zamówień']);
+ Permission::create(['name' => 'pages.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych stron']);
+ Permission::create(['name' => 'products.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych produktów']);
+ Permission::create(['name' => 'users.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych użytkowników']);
+
+ $owner = Role::query()->where('type', '=', RoleType::OWNER)->firstOrFail();
+ $owner->givePermissionTo([
+ 'orders.show_metadata_private',
+ 'pages.show_metadata_private',
+ 'products.show_metadata_private',
+ 'users.show_metadata_private',
+ ]);
+ $owner->save();
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ $owner = Role::query()->where('type', '=', RoleType::OWNER)->firstOrFail();
+ $owner->revokePermissionTo([
+ 'orders.show_metadata_private',
+ 'pages.show_metadata_private',
+ 'products.show_metadata_private',
+ 'users.show_metadata_private',
+ ]);
+ $owner->save();
+ }
+}
diff --git a/database/migrations/2022_03_17_160126_add_more_metadata_permissions.php b/database/migrations/2022_03_17_160126_add_more_metadata_permissions.php
new file mode 100644
index 000000000..53efe7706
--- /dev/null
+++ b/database/migrations/2022_03_17_160126_add_more_metadata_permissions.php
@@ -0,0 +1,77 @@
+ 'schemas.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych schematów']);
+ Permission::create(['name' => 'options.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych stron']);
+ Permission::create(['name' => 'product_sets.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych zestawów produktów']);
+ Permission::create(['name' => 'discounts.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych kuponów']);
+ Permission::create(['name' => 'items.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych przedmiotów']);
+ Permission::create(['name' => 'statuses.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych statusów']);
+ Permission::create(['name' => 'shipping_methods.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych metod dostawy']);
+ Permission::create(['name' => 'packages.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych szablonów paczek']);
+ Permission::create(['name' => 'roles.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych ról']);
+ Permission::create(['name' => 'apps.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych aplikacji']);
+ Permission::create(['name' => 'media.show_metadata_private', 'display_name' => 'Możliwość wyświetlania prywatnych metadanych mediów']);
+
+ $owner = Role::query()->where('type', '=', RoleType::OWNER)->firstOrFail();
+ $owner->givePermissionTo([
+ 'schemas.show_metadata_private',
+ 'options.show_metadata_private',
+ 'product_sets.show_metadata_private',
+ 'discounts.show_metadata_private',
+ 'items.show_metadata_private',
+ 'statuses.show_metadata_private',
+ 'shipping_methods.show_metadata_private',
+ 'packages.show_metadata_private',
+ 'roles.show_metadata_private',
+ 'apps.show_metadata_private',
+ 'media.show_metadata_private',
+ ]);
+ $owner->save();
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ $permissions = [
+ 'schemas.show_metadata_private',
+ 'options.show_metadata_private',
+ 'product_sets.show_metadata_private',
+ 'discounts.show_metadata_private',
+ 'items.show_metadata_private',
+ 'statuses.show_metadata_private',
+ 'shipping_methods.show_metadata_private',
+ 'packages.show_metadata_private',
+ 'roles.show_metadata_private',
+ 'apps.show_metadata_private',
+ 'media.show_metadata_private',
+ ];
+
+ $owner = Role::query()->where('type', '=', RoleType::OWNER)->firstOrFail();
+ $owner->revokePermissionTo($permissions);
+ $owner->save();
+
+ foreach ($permissions as $permission) {
+ Permission::query()
+ ->where('name', '=', $permission)
+ ->delete();
+ }
+ }
+}
diff --git a/database/migrations/2022_03_24_092349_create_product_attribute_attribute_option_table.php b/database/migrations/2022_03_24_092349_create_product_attribute_attribute_option_table.php
new file mode 100644
index 000000000..59a4184e0
--- /dev/null
+++ b/database/migrations/2022_03_24_092349_create_product_attribute_attribute_option_table.php
@@ -0,0 +1,56 @@
+dropForeign('product_attribute_option_id_foreign');
+
+ $table->dropColumn('option_id');
+ $table->dropPrimary(['product_id', 'attribute_id']);
+ $table->unique(['product_id', 'attribute_id']);
+ $table->uuid('id')->primary();
+ });
+
+ Schema::create('product_attribute_attribute_option', function (Blueprint $table) {
+ $table->uuid('product_attribute_id');
+ $table->uuid('attribute_option_id');
+
+ $table->foreign('product_attribute_id')->references('id')->on('product_attribute')->onDelete('cascade');
+ $table->foreign('attribute_option_id')->references('id')->on('attribute_options')->onDelete('cascade');
+
+ $table->primary(['product_attribute_id', 'attribute_option_id'], 'product_attribute_attribute_option_primary');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('product_attribute_attribute_option');
+
+ Schema::table('product_attribute', function (Blueprint $table) {
+ $table->dropPrimary('product_attribute_id_primary');
+ $table->dropColumn('id');
+
+ $table->dropUnique(['product_id', 'attribute_id']);
+ $table->primary(['product_id', 'attribute_id']);
+
+ $table->uuid('option_id');
+ $table->foreign('option_id')->references('id')->on('attribute_options')->onDelete('cascade');
+ });
+ }
+}
diff --git a/database/seeders/ProductSeeder.php b/database/seeders/ProductSeeder.php
index 45ecaf507..627740b34 100644
--- a/database/seeders/ProductSeeder.php
+++ b/database/seeders/ProductSeeder.php
@@ -2,12 +2,12 @@
namespace Database\Seeders;
-use App\Models\ProductSet;
use App\Models\Deposit;
use App\Models\Item;
use App\Models\Media;
use App\Models\Option;
use App\Models\Product;
+use App\Models\ProductSet;
use App\Models\Schema;
use App\Models\SeoMetadata;
use App\Services\Contracts\ProductServiceContract;
@@ -34,7 +34,7 @@ public function run()
$brands = ProductSet::factory([
'name' => 'Brands',
'slug' => 'brands',
- ])->create();
+ ])->make();
$this->seo($brands);
$brands = ProductSet::factory([
'parent_id' => $brands->getKey(),
@@ -71,10 +71,18 @@ public function run()
$this->categories($product, $categories);
}
+ $product->refresh();
+ $product->save();
$productService->updateMinMaxPrices($product);
});
}
+ private function seo(Product|ProductSet $product): void
+ {
+ $seo = SeoMetadata::factory()->create();
+ $product->seo()->save($seo);
+ }
+
private function schemas(Product $product): void
{
$schema = Schema::factory()->make();
@@ -101,19 +109,13 @@ private function sets(Product $product, Collection $sets): void
}
}
- private function categories(Product $product, Collection $categories): void
- {
- $product->sets()->syncWithoutDetaching($categories->random());
- }
-
private function brands(Product $product, Collection $brands): void
{
$product->sets()->syncWithoutDetaching($brands->random());
}
- private function seo(Product|ProductSet $product): void
+ private function categories(Product $product, Collection $categories): void
{
- $seo = SeoMetadata::factory()->create();
- $product->seo()->save($seo);
+ $product->sets()->syncWithoutDetaching($categories->random());
}
}
diff --git a/docker-compose.yaml b/docker-compose.yaml
index dd554987f..e8fa1469c 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -20,6 +20,9 @@ services:
touch /usr/src/init
fi
exec apache2-foreground
+ depends_on:
+ - mysql_service
+ - elasticsearch
mysql_service:
image: mariadb:10.5
restart: unless-stopped
@@ -27,27 +30,46 @@ services:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
ports:
- - ${DB_PORT}:3306
+ - ${DB_PORT:-3306}:3306
adminer:
image: adminer
restart: unless-stopped
environment:
- ADMINER_DEFAULT_SERVER=mysql_service
redis:
- image: redis:6.2
+ image: redis:5.0
restart: unless-stopped
ports:
- - ${REDIS_PORT}:6379
+ - ${REDIS_PORT:-6379}:6379
+ elasticsearch:
+ build:
+ context: ./docker
+ dockerfile: Dockerfile-elastic
+ ports:
+ - ${ELASTICSEARCH_PORT:-9200}:9200
+ environment:
+ - xpack.security.enabled=false
+ - discovery.type=single-node
+ kibana:
+ image: docker.elastic.co/kibana/kibana:8.1.0
+ environment:
+ - ELASTICSEARCH_HOSTS=http://elasticsearch:${ELASTICSEARCH_PORT:-9200}
+ depends_on:
+ - elasticsearch
nginx:
image: nginx
restart: unless-stopped
volumes:
- ./docker/nginx:/etc/nginx/templates
ports:
- - ${DOCKER_PORT}:80
+ - ${DOCKER_PORT:-80}:80
environment:
- - ADMINER_PREFIX=${DOCKER_ADMINER_PREFIX}
- - SILVERBOX_PREFIX=${DOCKER_SILVERBOX_PREFIX}
+ - ADMINER_PREFIX=${DOCKER_ADMINER_PREFIX:-adminer}
+ - KIBANA_PREFIX=${DOCKER_KIBANA_PREFIX:-kibana}
+ - SILVERBOX_PREFIX=${DOCKER_SILVERBOX_PREFIX:-silverbox}
+ depends_on:
+ - app
+ - kibana
silverbox:
image: heseya/silverbox:1.2.0
restart: unless-stopped
diff --git a/docker/Dockerfile-elastic b/docker/Dockerfile-elastic
new file mode 100644
index 000000000..3e950888a
--- /dev/null
+++ b/docker/Dockerfile-elastic
@@ -0,0 +1,3 @@
+FROM docker.elastic.co/elasticsearch/elasticsearch:8.1.0
+
+RUN bin/elasticsearch-plugin install pl.allegro.tech.elasticsearch.plugin:elasticsearch-analysis-morfologik:8.1.0
diff --git a/docker/nginx/default.conf.template b/docker/nginx/default.conf.template
index df0573fba..760a21bff 100644
--- a/docker/nginx/default.conf.template
+++ b/docker/nginx/default.conf.template
@@ -33,3 +33,16 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
+
+
+server {
+ listen 80;
+ server_name ${KIBANA_PREFIX}.*;
+
+ location / {
+ proxy_pass http://kibana:5601;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+}
diff --git a/heseya/pagination/.gitignore b/heseya/pagination/.gitignore
deleted file mode 100644
index 987e2a253..000000000
--- a/heseya/pagination/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-composer.lock
-vendor
diff --git a/heseya/pagination/composer.json b/heseya/pagination/composer.json
deleted file mode 100644
index a8deddbff..000000000
--- a/heseya/pagination/composer.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "name": "heseya/pagination",
- "description": "Heseya Store Pagination Middleware",
- "type": "library",
- "version": "1.0.0",
- "require": {
- "laravel/framework": "^9.0"
- },
- "autoload": {
- "psr-4": {
- "Heseya\\Pagination\\": "src/",
- "Heseya\\Pagination\\Tests\\": "tests/"
- }
- },
- "extra": {
- "laravel": {
- "providers": [
- "Heseya\\Pagination\\ServiceProvider"
- ]
- }
- },
- "authors": [
- {
- "name": "Jędrzej Buliński",
- "email": "jedrzej@heseya.com"
- }
- ]
-}
diff --git a/heseya/pagination/config/pagination.php b/heseya/pagination/config/pagination.php
deleted file mode 100644
index def7b28d5..000000000
--- a/heseya/pagination/config/pagination.php
+++ /dev/null
@@ -1,9 +0,0 @@
- (int) env('PAGINATION_DEFAULT', 24),
-
- 'max' => (int) env('PAGINATION_MAX', 500),
-
-];
diff --git a/heseya/pagination/src/Http/Middleware/Pagination.php b/heseya/pagination/src/Http/Middleware/Pagination.php
deleted file mode 100644
index f5e65cc82..000000000
--- a/heseya/pagination/src/Http/Middleware/Pagination.php
+++ /dev/null
@@ -1,34 +0,0 @@
-exists(self::LIMIT_NAME)) {
- $validator = Validator::make($request->only(self::LIMIT_NAME), [
- self::LIMIT_NAME => ['integer', 'min:1', 'max:' . Config::get('pagination.max')],
- ]);
-
- if ($validator->fails()) {
- throw new StoreException($validator->errors()->first());
- }
-
- Config::set('pagination.per_page', $request->input(self::LIMIT_NAME));
- }
-
- return $next($request);
- }
-}
diff --git a/heseya/pagination/src/ServiceProvider.php b/heseya/pagination/src/ServiceProvider.php
deleted file mode 100644
index 5f2ab5282..000000000
--- a/heseya/pagination/src/ServiceProvider.php
+++ /dev/null
@@ -1,14 +0,0 @@
-mergeConfigFrom(
- __DIR__ . '/../config/pagination.php',
- 'pagination',
- );
- }
-}
diff --git a/heseya/pagination/tests/Unit/MiddlewareTest.php b/heseya/pagination/tests/Unit/MiddlewareTest.php
deleted file mode 100644
index c04c342ba..000000000
--- a/heseya/pagination/tests/Unit/MiddlewareTest.php
+++ /dev/null
@@ -1,78 +0,0 @@
-handle($request, function (): void {
- });
-
- $this->assertEquals(Config::get('pagination.per_page'), 100);
- }
-
- public function testLimit(): void
- {
- $request = Request::create('/admin?limit=50', 'GET');
-
- $middleware = new Pagination();
- $middleware->handle($request, function (): void {
- });
-
- $this->assertEquals(Config::get('pagination.per_page'), 50);
- }
-
- public function testLimitValidation(): void
- {
- $request = Request::create('/admin?limit=TEST', 'GET');
-
- $middleware = new Pagination();
-
- $this->expectException(StoreException::class);
- $middleware->handle($request, function (): void {
- });
- }
-
- public function testLimitMax(): void
- {
- $request = Request::create('/admin?limit=1000', 'GET');
-
- $middleware = new Pagination();
-
- $this->expectException(StoreException::class);
- $middleware->handle($request, function (): void {
- });
- }
-
- public function testLimitMin(): void
- {
- $request = Request::create('/admin?limit=-1', 'GET');
-
- $middleware = new Pagination();
-
- $this->expectException(StoreException::class);
- $middleware->handle($request, function (): void {
- });
- }
-}
diff --git a/heseya/sortable/.gitignore b/heseya/sortable/.gitignore
deleted file mode 100644
index 8b7ef3503..000000000
--- a/heseya/sortable/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/vendor
-composer.lock
diff --git a/heseya/sortable/composer.json b/heseya/sortable/composer.json
deleted file mode 100644
index af65f7152..000000000
--- a/heseya/sortable/composer.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "heseya/sortable",
- "description": "Sortable Trait for Laravel",
- "type": "library",
- "license": "MIT",
- "version": "1.0.0",
- "autoload": {
- "psr-4": {
- "Heseya\\Sortable\\": "src/"
- }
- },
- "require": {
- "laravel/framework": "^9.0"
- }
-}
diff --git a/heseya/sortable/src/Sortable.php b/heseya/sortable/src/Sortable.php
deleted file mode 100644
index cefb5214a..000000000
--- a/heseya/sortable/src/Sortable.php
+++ /dev/null
@@ -1,57 +0,0 @@
- ['required', 'in:' . implode(',', $this->getSortable())],
- '1' => ['in:asc,desc'],
- ],
- [
- 'required' => 'You must specify sort field.',
- '0.in' => 'You can\'t sort by ' . $field . ' field.',
- '1.in' => 'Only asc|desc sorting directions are allowed on field ' . $field . '.',
- ]
- )->validate();
-
- $order = count($option) > 1 ? $option[1] : 'asc';
- $query->orderBy($field, $order);
- }
- }
-
- return $query->orderBy(
- $this->getDefaultSortBy(),
- $this->getDefaultSortDirection(),
- );
- }
-
- private function getSortable(): array
- {
- return $this->sortable ?? [];
- }
-
- private function getDefaultSortBy(): string
- {
- return $this->defaultSortBy ?? 'created_at';
- }
-
- private function getDefaultSortDirection(): string
- {
- return $this->defaultSortDirection ?? 'asc';
- }
-}
diff --git a/phpunit.xml b/phpunit.xml
index 65473a2ab..f1bd82c06 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -24,9 +24,6 @@
./tests/Feature
-
- ./heseya/pagination/tests/Unit
-
diff --git a/public/docs/api.yml b/public/docs/api.yml
index 43f39a8d7..cf9e80a93 100644
--- a/public/docs/api.yml
+++ b/public/docs/api.yml
@@ -176,6 +176,8 @@ paths:
items:
$ref: '#/components/schemas/Event'
type: object
+ /filters:
+ $ref: './paths/Filters.yml#/Filters'
/furgonetka/create-package:
$ref: './paths/Furgonetka.yml#/FurgonetkaCreatePackage'
/items:
@@ -650,6 +652,82 @@ paths:
$ref: './paths/Users.yml#/Users'
'/users/id:{id}':
$ref: './paths/Users.yml#/UsersParams'
+
+ '/products/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/products/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/schemas/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/schemas/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/options/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/options/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/product-sets/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/product-sets/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/discounts/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/discounts/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/items/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/items/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/orders/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/orders/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/statuses/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/statuses/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/shipping-methods/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/shipping-methods/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/package-templates/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/package-templates/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/users/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/users/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/roles/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/roles/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/pages/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/pages/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/apps/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/apps/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
+ '/media/id:{id}/metadata':
+ $ref: './paths/Metadata.yml#Metadata'
+ '/media/id:{id}/metadata-private':
+ $ref: './paths/Metadata.yml#Metadata'
+
'/users/id:{id}/2fa/remove':
$ref: './paths/Users.yml#/UserTFARemove'
/webhooks:
@@ -769,6 +847,14 @@ paths:
type: object
security:
- oauth: [ ]
+ '/attributes':
+ $ref: './paths/Attributes.yml#Attributes'
+ '/attributes/id:{id}':
+ $ref: './paths/Attributes.yml#AttributesParams'
+ '/attributes/id:{id}/options':
+ $ref: './paths/Attributes.yml#AttributeOption'
+ '/attributes/id:{attribute_id}/options/id:{option_id}':
+ $ref: './paths/Attributes.yml#AttributeOptionParams'
components:
schemas:
Error:
@@ -1117,6 +1203,10 @@ components:
$ref: './schemas/Seo.yml#SeoView'
SeoKeywordsResponse:
$ref: './schemas/Seo.yml#SeoKeywordsResponse'
+ Attribute:
+ $ref: './schemas/Attributes.yml#Attribute'
+ AttributeOption:
+ $ref: './schemas/Attributes.yml#AttributeOption'
requestBodies:
AppStore:
$ref: './requests/Apps.yml#/AppStore'
diff --git a/public/docs/paths/Attributes.yml b/public/docs/paths/Attributes.yml
new file mode 100644
index 000000000..24e85e896
--- /dev/null
+++ b/public/docs/paths/Attributes.yml
@@ -0,0 +1,164 @@
+Attributes:
+ get:
+ tags:
+ - Attributes
+ summary: 'list attributes'
+ responses:
+ 200:
+ description: Success
+ content:
+ application/json:
+ schema:
+ properties:
+ data:
+ type: array
+ items:
+ $ref: './../schemas/Attributes.yml#/Attribute'
+ type: object
+ security:
+ - oauth: [ ]
+ post:
+ tags:
+ - Attributes
+ summary: 'add new attribute'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: './../schemas/Attributes.yml#/Attribute'
+ responses:
+ 201:
+ description: Created
+ content:
+ application/json:
+ schema:
+ properties:
+ data:
+ $ref: './../schemas/Attributes.yml#/Attribute'
+ type: object
+ security:
+ - oauth: [ ]
+
+AttributesParams:
+ get:
+ tags:
+ - Attributes
+ summary: 'get attribute'
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ 200:
+ description: Success
+ content:
+ application/json:
+ schema:
+ properties:
+ data:
+ $ref: './../schemas/Attributes.yml#/Attribute'
+ type: object
+ security:
+ - oauth: [ ]
+ delete:
+ tags:
+ - Attributes
+ summary: 'delete attribute'
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ 204:
+ description: Success
+ security:
+ - oauth: [ ]
+ patch:
+ tags:
+ - Attributes
+ summary: 'update attribute'
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: './../schemas/Attributes.yml#/Attribute'
+ responses:
+ 200:
+ description: Success
+ content:
+ application/json:
+ schema:
+ properties:
+ data:
+ $ref: './../schemas/Attributes.yml#/Attribute'
+ type: object
+ security:
+ - oauth: [ ]
+
+AttributeOption:
+ post:
+ tags:
+ - Attributes
+ summary: 'add new option to attribute'
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: './../schemas/Attributes.yml#/AttributeOption'
+AttributeOptionParams:
+ patch:
+ tags:
+ - Attributes
+ summary: 'update option'
+ parameters:
+ - name: attribute_id
+ in: path
+ required: true
+ schema:
+ type: integer
+ - name: option_id
+ in: path
+ required: true
+ schema:
+ type: integer
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: './../schemas/Attributes.yml#/AttributeOption'
+ delete:
+ tags:
+ - Attributes
+ summary: 'delete option'
+ parameters:
+ - name: attribute_id
+ in: path
+ required: true
+ schema:
+ type: integer
+ - name: option_id
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ 204:
+ description: Success
+ security:
+ - oauth: [ ]
diff --git a/public/docs/paths/Filters.yml b/public/docs/paths/Filters.yml
new file mode 100644
index 000000000..6f58d3054
--- /dev/null
+++ b/public/docs/paths/Filters.yml
@@ -0,0 +1,14 @@
+Filters:
+ get:
+ tags:
+ - Filters
+ summary: 'gets sets filters'
+ parameters:
+ - name: 'sets[]'
+ in: query
+ description: 'Array of sets ids to search attributes'
+ schema:
+ type: string
+ example: 'sets[]=026bc5f6-8373-4aeb-972e-e78d72a67121&sets[]=026bc5f6-8373-4aeb-972e-e78d72a67121'
+ security:
+ - oauth: [ ]
diff --git a/public/docs/paths/Metadata.yml b/public/docs/paths/Metadata.yml
new file mode 100644
index 000000000..5665cd6ed
--- /dev/null
+++ b/public/docs/paths/Metadata.yml
@@ -0,0 +1,17 @@
+Metadata:
+ patch:
+ tags:
+ - Metadata
+ summary: 'Create/update/delete metadata'
+ requestBody:
+ $ref: './../requests/Metadata.yml#Metadata'
+ responses:
+ 200:
+ description: 'Returned list has whole metadata attached to requested object'
+ content:
+ application/json:
+ schema:
+ properties:
+ data:
+ $ref: './../schemas/Metadata.yml#Metadata'
+ type: object
diff --git a/public/docs/requests/Metadata.yml b/public/docs/requests/Metadata.yml
new file mode 100644
index 000000000..fa72e7e21
--- /dev/null
+++ b/public/docs/requests/Metadata.yml
@@ -0,0 +1,9 @@
+Metadata:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ faktura:
+ type: string
+ example: 'FV/2022/03/10/005'
diff --git a/public/docs/requests/ProductSets.yml b/public/docs/requests/ProductSets.yml
index a9ebdcc8d..f080c1bd6 100644
--- a/public/docs/requests/ProductSets.yml
+++ b/public/docs/requests/ProductSets.yml
@@ -46,6 +46,12 @@ ProductSetStore:
example: 026bc5f6-8373-4aeb-972e-e78d72a67121
seo:
$ref: './../schemas/Seo.yml#SeoStore'
+ attributes:
+ description: 'Array of assigned attributes ids'
+ type: array
+ items:
+ type: string
+ example: 026bc5f6-8373-4aeb-972e-e78d72a67121
type: object
ProductSetUpdate:
content:
@@ -95,6 +101,12 @@ ProductSetUpdate:
example: 026bc5f6-8373-4aeb-972e-e78d72a67121
seo:
$ref: './../schemas/Seo.yml#SeoStore'
+ attributes:
+ description: 'Array of assigned attributes ids'
+ type: array
+ items:
+ type: string
+ example: 026bc5f6-8373-4aeb-972e-e78d72a67121
type: object
ProductSetAttach:
content:
diff --git a/public/docs/requests/Products.yml b/public/docs/requests/Products.yml
index 0ca6d044d..25ab1d188 100644
--- a/public/docs/requests/Products.yml
+++ b/public/docs/requests/Products.yml
@@ -38,6 +38,10 @@ Product:
items:
type: string
example: 0006c3a0-21af-4485-b7fe-9c42233cf03a
+ metadata:
+ $ref: './../schemas/Metadata.yml#Metadata'
+ metadata_private:
+ $ref: './../schemas/Metadata.yml#Metadata'
ProductStore:
content:
@@ -53,6 +57,12 @@ ProductStore:
properties:
description_short:
type: string
+ attributes:
+ type: array
+ items:
+ type: string
+ description: attribute_id,option_id
+ example: 0006c3a0-21af-4485-b7fe-9c42233cf03a,0006c654-21bn-44rt-b7fe-9c4uyji8f03a
type: object
ProductUpdate:
diff --git a/public/docs/schemas/Attributes.yml b/public/docs/schemas/Attributes.yml
new file mode 100644
index 000000000..f1257593e
--- /dev/null
+++ b/public/docs/schemas/Attributes.yml
@@ -0,0 +1,118 @@
+Attribute:
+ type: object
+ properties:
+ id:
+ type: string
+ example: 026bc5f6-8373-4aeb-972e-e78d72a67121
+ name:
+ description: 'Name of attribute'
+ type: string
+ example: 'Screen size'
+ slug:
+ description: 'Slug of name attribute'
+ type: string
+ example: 'screen-size'
+ description:
+ description: 'Description of attribute'
+ type: string
+ example: 'Presented number is size of screen monitor'
+ min:
+ description: 'Lowest value from options'
+ type: integer
+ example: '22'
+ max:
+ description: 'Highest value from options'
+ type: integer
+ example: '27'
+ type:
+ description: 'Type of attribute'
+ type: integer
+ example: 1
+ global:
+ description: 'Possibility to use attribute in search'
+ type: boolean
+ example: 'true'
+ sortable:
+ description: 'Parameter for storefront'
+ type: boolean
+ example: 'true'
+ options:
+ description: 'Array options of attribute'
+ type: array
+ items:
+ type: object
+ properties:
+ value_text:
+ description: 'Text of attribute option'
+ type: string
+ example: 'inch'
+ value:
+ description: 'Value of attribute option'
+ type: float
+ example: '27'
+
+AttributeOption:
+ type: object
+ properties:
+ id:
+ type: string
+ example: 026bc5f6-8373-4aeb-972e-e78d72a67121
+ name:
+ description: 'Name of attribute option'
+ type: string
+ example: 'Screen size'
+ index:
+ description: 'Index of attribute option'
+ type: integer
+ example: 1
+ value_number:
+ description: 'Number value attribute option'
+ type: float
+ example: '27'
+ value_date:
+ description: 'Date value attribute option'
+ type: date
+ example: '2022-02-22'
+ attribute_id:
+ type: string
+ example: 026bc5f6-8373-4aeb-972e-e78d72a67121
+
+AttributeProductList:
+ type: object
+ properties:
+ name:
+ description: 'Name of attribute'
+ type: string
+ example: 'Screen size'
+ selected_options:
+ description: 'Array options of attribute'
+ $ref: '#AttributeOption'
+
+AttributeProduct:
+ type: object
+ allOf:
+ - $ref: '#AttributeProductList'
+ - properties:
+ id:
+ type: string
+ example: 026bc5f6-8373-4aeb-972e-e78d72a67121
+ slug:
+ description: 'Slug of name attribute'
+ type: string
+ example: 'screen-size'
+ description:
+ description: 'Description of attribute'
+ type: string
+ example: 'Presented number is size of screen monitor'
+ type:
+ description: 'Type of attribute'
+ type: integer
+ example: 1
+ global:
+ description: 'Possibility to use attribute in search'
+ type: boolean
+ example: 'true'
+ sortable:
+ description: 'Parameter for storefront'
+ type: boolean
+ example: 'true'
diff --git a/public/docs/schemas/Metadata.yml b/public/docs/schemas/Metadata.yml
new file mode 100644
index 000000000..d26cbd40e
--- /dev/null
+++ b/public/docs/schemas/Metadata.yml
@@ -0,0 +1,12 @@
+Metadata:
+ type: object
+ properties:
+ invoice:
+ type: string
+ example: 'FV/2022/03/10/005'
+ erp_id:
+ type: number
+ example: 132432153
+ synced:
+ type: boolean
+ example: true
diff --git a/public/docs/schemas/Products.yml b/public/docs/schemas/Products.yml
index f67ed2c40..7f3ae76d3 100644
--- a/public/docs/schemas/Products.yml
+++ b/public/docs/schemas/Products.yml
@@ -46,6 +46,11 @@ Product:
type: array
items:
$ref: './Tags.yml#/Tag'
+ attributes:
+ description: 'Array of assigned attributes'
+ type: array
+ items:
+ $ref: './Attributes.yml#/AttributeProductList'
items:
description: 'Ids of assigned items'
type: array
@@ -89,5 +94,10 @@ ProductView:
$ref: './ProductSets.yml#/ProductSet'
seo:
$ref: './Seo.yml#/SeoView'
+ attributes:
+ description: 'Array of assigned attributes'
+ type: array
+ items:
+ $ref: './Attributes.yml#/AttributeProduct'
items:
$ref: './Items.yml#/ItemWithRequiredQuantity'
diff --git a/routes/app.php b/routes/app.php
index 30629eeff..75952638c 100644
--- a/routes/app.php
+++ b/routes/app.php
@@ -1,6 +1,7 @@
group(function (): void {
@@ -10,6 +11,10 @@
->middleware('can:apps.show_details');
Route::post(null, [AppController::class, 'store'])
->middleware('can:apps.install');
+ Route::patch('id:{app:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:apps.install');
+ Route::patch('id:{app:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:apps.install');
Route::delete('id:{app:id}', [AppController::class, 'destroy'])
->middleware('can:apps.remove');
});
diff --git a/routes/attribute.php b/routes/attribute.php
new file mode 100644
index 000000000..1c9b0c433
--- /dev/null
+++ b/routes/attribute.php
@@ -0,0 +1,24 @@
+group(function (): void {
+ Route::get(null, [AttributeController::class, 'index'])
+ ->middleware('permission:attributes.show');
+ Route::post(null, [AttributeController::class, 'store'])
+ ->middleware('permission:attributes.add');
+ Route::get('id:{attribute:id}', [AttributeController::class, 'show'])
+ ->middleware('permission:attributes.show');
+ Route::patch('id:{attribute:id}', [AttributeController::class, 'update'])
+ ->middleware('permission:attributes.edit');
+ Route::delete('id:{attribute:id}', [AttributeController::class, 'destroy'])
+ ->middleware('can:attributes.remove');
+ Route::post('id:{attribute:id}/options', [AttributeOptionController::class, 'store'])
+ ->middleware('permission:attributes.edit');
+ Route::patch('id:{attribute:id}/options/id:{option:id}', [AttributeOptionController::class, 'update'])
+ ->middleware('permission:attributes.edit');
+ Route::delete('id:{attribute:id}/options/id:{option:id}', [AttributeOptionController::class, 'destroy'])
+ ->middleware('permission:attributes.edit');
+});
diff --git a/routes/discount.php b/routes/discount.php
index 955bab15c..0f8966318 100644
--- a/routes/discount.php
+++ b/routes/discount.php
@@ -1,6 +1,7 @@
group(function (): void {
@@ -12,6 +13,10 @@
->middleware('can:discounts.add');
Route::patch('id:{discount:id}', [DiscountController::class, 'update'])
->middleware('can:discounts.edit');
+ Route::patch('id:{discount:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:discounts.edit');
+ Route::patch('id:{discount:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:discounts.edit');
Route::delete('id:{discount:id}', [DiscountController::class, 'destroy'])
->middleware('can:discounts.remove');
});
diff --git a/routes/filters.php b/routes/filters.php
new file mode 100644
index 000000000..78440f1ea
--- /dev/null
+++ b/routes/filters.php
@@ -0,0 +1,6 @@
+group(function (): void {
@@ -13,6 +14,10 @@
->middleware('can:items.show_details');
Route::patch('id:{item:id}', [ItemController::class, 'update'])
->middleware('can:items.edit');
+ Route::patch('id:{item:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:items.edit');
+ Route::patch('id:{item:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:items.edit');
Route::delete('id:{item:id}', [ItemController::class, 'destroy'])
->middleware('can:items.remove');
diff --git a/routes/media.php b/routes/media.php
index 99a9f70f8..de7b56c64 100644
--- a/routes/media.php
+++ b/routes/media.php
@@ -1,6 +1,7 @@
group(function (): void {
@@ -10,6 +11,10 @@
->middleware($auth);
Route::patch('id:{media:id}', [MediaController::class, 'update'])
->middleware($auth);
+ Route::patch('id:{media:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware($auth);
+ Route::patch('id:{media:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware($auth);
Route::delete('id:{media:id}', [MediaController::class, 'destroy'])
->middleware($auth);
});
diff --git a/routes/option.php b/routes/option.php
new file mode 100644
index 000000000..5a5054c80
--- /dev/null
+++ b/routes/option.php
@@ -0,0 +1,11 @@
+group(function (): void {
+ Route::patch('id:{option:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('permission:products.edit');
+ Route::patch('id:{option:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('permission:products.edit');
+});
diff --git a/routes/order.php b/routes/order.php
index 41cf73f4c..b8d57231a 100644
--- a/routes/order.php
+++ b/routes/order.php
@@ -1,5 +1,6 @@
middleware('can:orders.edit.status');
Route::patch('id:{order:id}', [OrderController::class, 'update'])
->middleware('can:orders.edit');
+ Route::patch('id:{order:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:orders.edit');
+ Route::patch('id:{order:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:orders.edit');
Route::get('{order:code}', [OrderController::class, 'showPublic'])
->middleware('can:orders.show_summary');
diff --git a/routes/package-template.php b/routes/package-template.php
index 463e62c21..b17d02030 100644
--- a/routes/package-template.php
+++ b/routes/package-template.php
@@ -1,5 +1,6 @@
middleware('can:packages.add');
Route::patch('id:{package:id}', [PackageTemplateController::class, 'update'])
->middleware('can:packages.edit');
+ Route::patch('id:{package:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:packages.edit');
+ Route::patch('id:{package:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:packages.edit');
Route::delete('id:{package:id}', [PackageTemplateController::class, 'destroy'])
->middleware('can:packages.remove');
});
diff --git a/routes/page.php b/routes/page.php
index 12a46870f..39668b4e8 100644
--- a/routes/page.php
+++ b/routes/page.php
@@ -1,5 +1,6 @@
middleware('can:pages.show_details');
Route::patch('id:{page:id}', [PageController::class, 'update'])
->middleware('can:pages.edit');
+ Route::patch('id:{page:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:pages.edit');
+ Route::patch('id:{page:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:pages.edit');
Route::delete('id:{page:id}', [PageController::class, 'destroy'])
->middleware('can:pages.remove');
Route::post('reorder', [PageController::class, 'reorder'])
diff --git a/routes/product-set.php b/routes/product-set.php
index b5a83c3ae..e0b2cda8d 100644
--- a/routes/product-set.php
+++ b/routes/product-set.php
@@ -1,5 +1,6 @@
middleware('can:product_sets.add');
Route::patch('id:{product_set:id}', [ProductSetController::class, 'update'])
->middleware('can:product_sets.edit');
+ Route::patch('id:{product_set:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:product_sets.edit');
+ Route::patch('id:{product_set:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:product_sets.edit');
Route::post('reorder', [ProductSetController::class, 'reorder'])
->middleware('can:product_sets.edit');
Route::post('reorder/id:{product_set:id}', [ProductSetController::class, 'reorder'])
diff --git a/routes/product.php b/routes/product.php
index 4f81c77e8..dbf99d56c 100644
--- a/routes/product.php
+++ b/routes/product.php
@@ -1,5 +1,6 @@
middleware('can:products.show_details');
Route::patch('id:{product:id}', [ProductController::class, 'update'])
->middleware('can:products.edit');
+ Route::patch('id:{product:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:products.edit');
+ Route::patch('id:{product:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:products.edit');
Route::delete('id:{product:id}', [ProductController::class, 'destroy'])
->middleware('can:products.remove');
});
diff --git a/routes/role.php b/routes/role.php
index 7f8e4a31c..752816e12 100644
--- a/routes/role.php
+++ b/routes/role.php
@@ -1,5 +1,6 @@
middleware('can:roles.show_details');
Route::patch('id:{role:id}', [RoleController::class, 'update'])
->middleware('can:roles.edit');
+ Route::patch('id:{role:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:roles.edit');
+ Route::patch('id:{role:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:roles.edit');
Route::delete('id:{role:id}', [RoleController::class, 'destroy'])
->middleware('can:roles.remove');
});
diff --git a/routes/schema.php b/routes/schema.php
index 7efe058ca..042fec461 100644
--- a/routes/schema.php
+++ b/routes/schema.php
@@ -1,5 +1,6 @@
middleware('permission:products.add|products.edit');
Route::patch('id:{schema:id}', [SchemaController::class, 'update'])
->middleware('permission:products.add|products.edit');
+ Route::patch('id:{schema:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('permission:products.edit');
+ Route::patch('id:{schema:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('permission:products.edit');
Route::delete('id:{schema:id}', [SchemaController::class, 'destroy'])
->middleware('can:schemas.remove');
});
diff --git a/routes/shipping-method.php b/routes/shipping-method.php
index 8c6b735ef..bb53131a2 100644
--- a/routes/shipping-method.php
+++ b/routes/shipping-method.php
@@ -1,5 +1,6 @@
middleware('can:shipping_methods.add');
Route::patch('id:{shipping_method:id}', [ShippingMethodController::class, 'update'])
->middleware('can:shipping_methods.edit');
+ Route::patch('id:{shipping_method:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:shipping_methods.edit');
+ Route::patch('id:{shipping_method:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:shipping_methods.edit');
Route::delete('id:{shipping_method:id}', [ShippingMethodController::class, 'destroy'])
->middleware('can:shipping_methods.remove');
Route::post('reorder', [ShippingMethodController::class, 'reorder'])
diff --git a/routes/status.php b/routes/status.php
index d0634f3c8..4a4cfff8f 100644
--- a/routes/status.php
+++ b/routes/status.php
@@ -1,5 +1,6 @@
middleware('can:statuses.add');
Route::patch('id:{status:id}', [StatusController::class, 'update'])
->middleware('can:statuses.edit');
+ Route::patch('id:{status:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:statuses.edit');
+ Route::patch('id:{status:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:statuses.edit');
Route::post('reorder', [StatusController::class, 'reorder'])
->middleware('can:statuses.edit');
Route::delete('id:{status:id}', [StatusController::class, 'destroy'])
diff --git a/routes/user.php b/routes/user.php
index c65196060..706921000 100644
--- a/routes/user.php
+++ b/routes/user.php
@@ -1,6 +1,7 @@
middleware('can:users.add');
Route::patch('id:{user:id}', [UserController::class, 'update'])
->middleware('can:users.edit');
+ Route::patch('id:{user:id}/metadata', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:users.edit');
+ Route::patch('id:{user:id}/metadata-private', [MetadataController::class, 'updateOrCreate'])
+ ->middleware('can:users.edit');
Route::delete('id:{user:id}', [UserController::class, 'destroy'])
->middleware('can:users.remove');
});
diff --git a/tests/CreatesApplication.php b/tests/CreatesApplication.php
index b3365feb7..d09614712 100644
--- a/tests/CreatesApplication.php
+++ b/tests/CreatesApplication.php
@@ -19,6 +19,7 @@ trait CreatesApplication
'view:clear',
'config:clear',
'route:clear',
+ 'scout:index products',
];
/**
diff --git a/tests/Feature/AppInstallTest.php b/tests/Feature/AppInstallTest.php
index b3fa831bf..db991ddc2 100644
--- a/tests/Feature/AppInstallTest.php
+++ b/tests/Feature/AppInstallTest.php
@@ -246,6 +246,7 @@ public function testInstall($user): void
'version' => '1.0.0',
'description' => 'Cool description',
'icon' => 'https://picsum.photos/200',
+ 'metadata' => [],
]);
$this->assertDatabaseHas('apps', [
@@ -374,6 +375,7 @@ public function testInstallWithOptionalPermissions($user): void
'version' => '1.0.0',
'description' => 'Cool description',
'icon' => 'https://picsum.photos/200',
+ 'metadata' => [],
]);
$this->assertDatabaseHas('apps', [
diff --git a/tests/Feature/AppOtherTest.php b/tests/Feature/AppOtherTest.php
index 5b95fa42a..0d971a701 100644
--- a/tests/Feature/AppOtherTest.php
+++ b/tests/Feature/AppOtherTest.php
@@ -67,6 +67,7 @@ public function testShow(): void
'icon' => $app->icon,
'author' => $app->author,
'permissions' => [],
+ 'metadata' => [],
],
]);
}
diff --git a/tests/Feature/AttributeTest.php b/tests/Feature/AttributeTest.php
new file mode 100644
index 000000000..ab6e28078
--- /dev/null
+++ b/tests/Feature/AttributeTest.php
@@ -0,0 +1,1159 @@
+attribute = Attribute::factory()->create();
+
+ $this->option = AttributeOption::factory()->create([
+ 'index' => 1,
+ 'attribute_id' => $this->attribute->getKey(),
+ ]);
+
+ $this->attribute->refresh();
+
+ $this->newAttribute = Attribute::factory()->definition();
+ $this->newAttribute['options'] = [
+ AttributeOption::factory()->definition(),
+ AttributeOption::factory()->definition(),
+ ];
+
+ $this->newOption = AttributeOption::factory()->definition();
+
+ $this->expectedStructure = [
+ 'data' => [
+ 'name',
+ 'slug',
+ 'description',
+ 'min',
+ 'max',
+ 'type',
+ 'global',
+ 'sortable',
+ 'options',
+ ],
+ ];
+ }
+
+ public function testIndexUnauthorized(): void
+ {
+ $response = $this->getJson('/attributes');
+ $response->assertForbidden();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testIndex($user): void
+ {
+ $this->$user->givePermissionTo('attributes.show');
+
+ $this->newAttribute['global'] = !$this->attribute->global;
+ unset($this->newAttribute['options']);
+ Attribute::create($this->newAttribute);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/attributes')
+ ->assertOk()
+ ->assertJsonCount(2, 'data')
+ ->assertJsonFragment([
+ 'name' => $this->attribute->name,
+ 'slug' => $this->attribute->slug,
+ 'description' => $this->attribute->description,
+ 'type' => $this->attribute->type,
+ 'global' => $this->attribute->global,
+ 'sortable' => $this->attribute->sortable,
+ ])
+ ->assertJsonFragment([
+ 'index' => $this->option->index,
+ 'name' => $this->option->name,
+ 'value_number' => $this->option->value_number,
+ 'value_date' => $this->option->value_date,
+ ])
+ ->assertJsonFragment($this->newAttribute);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testIndexGlobalFlagTrue($user): void
+ {
+ $this->$user->givePermissionTo('attributes.show');
+
+ $this->newAttribute['global'] = true;
+ unset($this->newAttribute['options']);
+ Attribute::create($this->newAttribute);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/attributes?global=1')
+ ->assertOk()
+ ->assertJsonMissing(['global' => false])
+ ->assertJsonFragment($this->newAttribute);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testIndexGlobalFlagFalse($user): void
+ {
+ $this->$user->givePermissionTo('attributes.show');
+
+ $this->newAttribute['global'] = false;
+ unset($this->newAttribute['options']);
+ Attribute::create($this->newAttribute);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/attributes?global=0')
+ ->assertOk()
+ ->assertJsonMissing(['global' => true])
+ ->assertJsonFragment($this->newAttribute);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testShow($user): void
+ {
+ $this->$user->givePermissionTo('attributes.show');
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/attributes/id:'. $this->attribute->getKey())
+ ->assertOk()
+ ->assertJsonFragment([
+ 'name' => $this->attribute->name,
+ 'slug' => $this->attribute->slug,
+ 'description' => $this->attribute->description,
+ 'type' => $this->attribute->type,
+ 'global' => $this->attribute->global,
+ 'sortable' => $this->attribute->sortable,
+ ])
+ ->assertJsonFragment([
+ 'index' => $this->option->index,
+ 'name' => $this->option->name,
+ 'value_number' => $this->option->value_number,
+ 'value_date' => $this->option->value_date,
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testShowMinMaxNumber($user): void
+ {
+ $this->$user->givePermissionTo('attributes.show');
+
+ $attribute = Attribute::create([
+ 'name' => 'Monitor screen size',
+ 'slug' => 'monitor-screen-size',
+ 'description' => 'MinMax attribute number description',
+ 'type' => AttributeType::NUMBER,
+ 'global' => true,
+ 'sortable' => true,
+ ]);
+
+ $option1 = AttributeOption::create([
+ 'index' => 1,
+ 'name' => 'Modern screen size',
+ 'value_number' => 27,
+ 'value_date' => null,
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $option2 = AttributeOption::create([
+ 'index' => 2,
+ 'name' => 'Old screen size',
+ 'value_number' => 15,
+ 'value_date' => null,
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/attributes/id:'. $attribute->getKey())
+ ->assertOk()
+ ->assertJsonFragment([
+ 'name' => $attribute->name,
+ 'slug' => $attribute->slug,
+ 'description' => $attribute->description,
+ 'min' => $option2->value_number,
+ 'max' => $option1->value_number,
+ 'type' => $attribute->type,
+ 'global' => $attribute->global,
+ 'sortable' => $attribute->sortable,
+ ])
+ ->assertJsonFragment([
+ 'index' => $option1->index,
+ 'name' => $option1->name,
+ 'value_number' => $option1->value_number,
+ 'value_date' => $option1->value_date,
+ 'attribute_id' => $option1->attribute_id,
+ ])
+ ->assertJsonFragment([
+ 'index' => $option2->index,
+ 'name' => $option2->name,
+ 'value_number' => $option2->value_number,
+ 'value_date' => $option2->value_date,
+ 'attribute_id' => $option2->attribute_id,
+ ]);
+
+ //checking rest of min/max fields in attribute
+ $this->assertDatabaseHas('attributes', [
+ 'id' => $attribute->getKey(),
+ 'min_date' => null,
+ 'max_date' => null,
+ ]);
+ //making sure to other attributes was not updated by this one
+ $this->assertDatabaseMissing('attributes', [
+ 'id' => $this->attribute->getKey(),
+ 'min_number' => $option2->value_number,
+ 'max_number' => $option1->value_number,
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testShowMinMaxDate($user): void
+ {
+ $this->$user->givePermissionTo('attributes.show');
+
+ $attribute = Attribute::create([
+ 'name' => 'Release date',
+ 'slug' => 'release-date',
+ 'description' => 'MinMax attribute date description',
+ 'type' => AttributeType::DATE,
+ 'global' => true,
+ 'sortable' => true,
+ ]);
+
+ $option1 = AttributeOption::create([
+ 'index' => 1,
+ 'name' => 'Book #1',
+ 'value_number' => null,
+ 'value_date' => '2000-03-25',
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $option2 = AttributeOption::create([
+ 'index' => 2,
+ 'name' => 'Book #2',
+ 'value_number' => null,
+ 'value_date' => '1999-02-01',
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/attributes/id:'. $attribute->getKey())
+ ->assertOk()
+ ->assertJsonFragment([
+ 'name' => $attribute->name,
+ 'slug' => $attribute->slug,
+ 'description' => $attribute->description,
+ 'min' => $option2->value_date,
+ 'max' => $option1->value_date,
+ 'type' => $attribute->type,
+ 'global' => $attribute->global,
+ 'sortable' => $attribute->sortable,
+ ])
+ ->assertJsonFragment([
+ 'index' => $option1->index,
+ 'name' => $option1->name,
+ 'value_number' => $option1->value_number,
+ 'value_date' => $option1->value_date,
+ 'attribute_id' => $option1->attribute_id,
+ ])
+ ->assertJsonFragment([
+ 'index' => $option2->index,
+ 'name' => $option2->name,
+ 'value_number' => $option2->value_number,
+ 'value_date' => $option2->value_date,
+ 'attribute_id' => $option2->attribute_id,
+ ]);
+
+ //checking rest of min/max fields in attribute
+ $this->assertDatabaseHas('attributes', [
+ 'id' => $attribute->getKey(),
+ 'min_number' => null,
+ 'max_number' => null,
+ ]);
+ //making sure to other attributes was not updated by this one
+ $this->assertDatabaseMissing('attributes', [
+ 'id' => $this->attribute->getKey(),
+ 'min_date' => $option2->value_date,
+ 'max_date' => $option1->value_date,
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testCreate($user): void
+ {
+ $this->$user->givePermissionTo('attributes.add');
+
+ $this
+ ->actingAs($this->$user)
+ ->postJson('/attributes', $this->newAttribute)
+ ->assertCreated()
+ ->assertJsonStructure($this->expectedStructure)
+ ->assertJsonFragment([
+ 'name' => $this->newAttribute['name'],
+ 'slug' => $this->newAttribute['slug'],
+ 'description' => $this->newAttribute['description'],
+ 'type' => $this->newAttribute['type'],
+ 'global' => $this->newAttribute['global'],
+ 'sortable' => $this->newAttribute['sortable'],
+ ])
+ ->assertJsonFragment(['index' => 1] + $this->newAttribute['options'][0])
+ ->assertJsonFragment(['index' => 2] + $this->newAttribute['options'][1]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testCreateSingleOptionAndOptionWithoutName($user): void
+ {
+ $this->$user->givePermissionTo('attributes.add');
+
+ $this->newAttribute['type'] = AttributeType::SINGLE_OPTION;
+ unset($this->newAttribute['options']);
+ unset($this->newOption['name']);
+ $this->newAttribute['options'] = [$this->newOption];
+
+ $this
+ ->actingAs($this->$user)
+ ->postJson('/attributes', $this->newAttribute)
+ ->assertUnprocessable();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testCreateWithInvalidValueNumber($user): void
+ {
+ $this->$user->givePermissionTo('attributes.add');
+
+ $attribute = Attribute::factory()->make([
+ 'type' => AttributeType::SINGLE_OPTION,
+ ]);
+ $attribute['options'] = [
+ AttributeOption::factory()->make([
+ 'value_number' => 9999999.99,
+ ]),
+ ];
+
+ $response = $this
+ ->actingAs($this->$user)
+ ->postJson('/attributes', $attribute->toArray());
+
+ $response->assertUnprocessable();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testCreateIncompleteData($user): void
+ {
+ $this->$user->givePermissionTo('attributes.add');
+
+ unset($this->newAttribute['name']);
+
+ $this
+ ->actingAs($this->$user)
+ ->postJson('/attributes', $this->newAttribute)
+ ->assertUnprocessable();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testCreateUnauthorized($user): void
+ {
+ $this
+ ->actingAs($this->$user)
+ ->postJson('/attributes', $this->newAttribute)
+ ->assertForbidden();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdate($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $attributeUpdate = [
+ 'name' => 'Test ' . $this->attribute->name,
+ 'slug' => 'test-' . $this->attribute->slug,
+ 'description' => 'Test ' . $this->attribute->description,
+ 'type' => $this->attribute->type,
+ 'global' => true,
+ 'sortable' => true,
+ 'options' => [
+ [
+ 'id' => $this->option->getKey(),
+ 'name' => 'Test ' . $this->option->name,
+ 'value_number' => $this->option->value_number,
+ 'value_date' => $this->option->value_date,
+ ],
+ ],
+ ];
+
+ $this
+ ->actingAs($this->$user)
+ ->patchJson('/attributes/id:' . $this->attribute->getKey(), $attributeUpdate)
+ ->assertOk()
+ ->assertJsonStructure($this->expectedStructure)
+ ->assertJsonFragment([
+ 'name' => $attributeUpdate['name'],
+ 'slug' => $attributeUpdate['slug'],
+ 'description' => $attributeUpdate['description'],
+ 'type' => $attributeUpdate['type'],
+ 'global' => $attributeUpdate['global'],
+ 'sortable' => $attributeUpdate['sortable'],
+ ])
+ ->assertJsonFragment($attributeUpdate['options'][0]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateWithoutSlug($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $attributeUpdate = [
+ 'name' => 'Test ' . $this->attribute->name,
+ 'slug' => $this->attribute->slug,
+ 'description' => 'Test ' . $this->attribute->description,
+ 'type' => $this->attribute->type,
+ 'global' => true,
+ 'sortable' => true,
+ 'options' => [
+ [
+ 'id' => $this->option->getKey(),
+ 'name' => 'Test ' . $this->option->name,
+ 'value_number' => $this->option->value_number,
+ 'value_date' => $this->option->value_date,
+ ],
+ ],
+ ];
+
+ $this
+ ->actingAs($this->$user)
+ ->patchJson('/attributes/id:' . $this->attribute->getKey(), $attributeUpdate)
+ ->assertOk()
+ ->assertJsonStructure($this->expectedStructure)
+ ->assertJsonFragment([
+ 'name' => $attributeUpdate['name'],
+ 'slug' => $attributeUpdate['slug'],
+ 'description' => $attributeUpdate['description'],
+ 'type' => $attributeUpdate['type'],
+ 'global' => $attributeUpdate['global'],
+ 'sortable' => $attributeUpdate['sortable'],
+ ])
+ ->assertJsonFragment($attributeUpdate['options'][0]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateChangeType($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ while (true) {
+ $randomType = AttributeType::getRandomValue();
+
+ if ($randomType !== $this->attribute->type->value) {
+ $this->attribute->type = $randomType;
+ break;
+ }
+ }
+
+ $attributeUpdate = [
+ 'name' => 'Test ' . $this->attribute->name,
+ 'slug' => $this->attribute->slug,
+ 'description' => 'Test ' . $this->attribute->description,
+ 'type' => $this->attribute->type,
+ 'global' => true,
+ 'sortable' => true,
+ 'options' => [
+ [
+ 'id' => $this->option->getKey(),
+ 'name' => 'Test ' . $this->option->name,
+ 'value_number' => $this->option->value_number,
+ 'value_date' => $this->option->value_date,
+ ],
+ ],
+ ];
+
+ $this
+ ->actingAs($this->$user)
+ ->patchJson('/attributes/id:' . $this->attribute->getKey(), $attributeUpdate)
+ ->assertUnprocessable();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateIncompleteData($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $attributeUpdate = [
+ 'name' => 'Test update attribute name',
+ ];
+
+ $this
+ ->actingAs($this->$user)
+ ->patchJson('/attributes/id:' . $this->attribute->getKey(), $attributeUpdate)
+ ->assertUnprocessable();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateNotExistingAttribute($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ Attribute::destroy($this->attribute->getKey());
+
+ $attributeUpdate = [
+ 'name' => 'Test update attribute name',
+ ];
+
+ $this
+ ->actingAs($this->$user)
+ ->patchJson('/attributes/id:' . $this->attribute->getKey(), $attributeUpdate)
+ ->assertNotFound();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateWithoutAssignedOption($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $attributeUpdate = [
+ 'name' => 'Test ' . $this->attribute->name,
+ 'slug' => 'test-' . $this->attribute->slug,
+ 'description' => 'Test ' . $this->attribute->description,
+ 'type' => $this->attribute->type,
+ 'global' => true,
+ 'sortable' => true,
+ 'options' => [
+ [
+ 'name' => 'Totally different option',
+ 'value_number' => $this->option->value_number,
+ 'value_date' => $this->option->value_date,
+ ],
+ ],
+ ];
+
+ $this
+ ->actingAs($this->$user)
+ ->patchJson('/attributes/id:' . $this->attribute->getKey(), $attributeUpdate)
+ ->assertOk()
+ ->assertJsonStructure($this->expectedStructure)
+ ->assertJsonFragment([
+ 'name' => $attributeUpdate['name'],
+ 'slug' => $attributeUpdate['slug'],
+ 'description' => $attributeUpdate['description'],
+ 'type' => $attributeUpdate['type'],
+ 'global' => $attributeUpdate['global'],
+ 'sortable' => $attributeUpdate['sortable'],
+ ])
+ ->assertJsonFragment($attributeUpdate['options'][0])
+ ->assertJsonMissing(['id' => $this->option->getKey()]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateUnauthorized($user): void
+ {
+ $attributeUpdate = [
+ 'name' => 'Test ' . $this->attribute->name,
+ 'slug' => 'test-' . $this->attribute->slug,
+ 'description' => 'Test ' . $this->attribute->description,
+ 'type' => AttributeType::NUMBER,
+ 'global' => true,
+ 'sortable' => true,
+ 'options' => [
+ [
+ 'id' => $this->option->id,
+ 'name' => 'Test ' . $this->option->name,
+ 'value_number' => $this->option->value_number,
+ 'value_date' => $this->option->value_date,
+ ],
+ ],
+ ];
+
+ $this
+ ->actingAs($this->$user)
+ ->patchJson('/attributes/id:' . $this->attribute->getKey(), $attributeUpdate)
+ ->assertForbidden();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testDelete($user): void
+ {
+ $this->$user->givePermissionTo('attributes.remove');
+
+ $this
+ ->actingAs($this->$user)
+ ->deleteJson('/attributes/id:' . $this->attribute->getKey())
+ ->assertNoContent();
+
+ $this->assertDatabaseMissing('attributes', [
+ 'id' => $this->attribute->getKey(),
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testDeleteNotExistingAttribute($user): void
+ {
+ $this->$user->givePermissionTo('attributes.remove');
+
+ Attribute::destroy($this->attribute->getKey());
+
+ $this
+ ->actingAs($this->$user)
+ ->deleteJson('/attributes/id:' . $this->attribute->getKey())
+ ->assertNotFound();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testDeleteUnauthorized($user): void
+ {
+ $this
+ ->actingAs($this->$user)
+ ->deleteJson('/attributes/id:' . $this->attribute->getKey())
+ ->assertForbidden();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testAddOption($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $this
+ ->actingAs($this->$user)
+ ->postJson('/attributes/id:' . $this->attribute->getKey() . '/options', $this->newOption)
+ ->assertCreated()
+ ->assertJsonFragment($this->newOption);
+
+ $this->assertDatabaseHas('attribute_options', $this->newOption);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testAddOptionNumberWithoutName($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $attribute = Attribute::factory([
+ 'type' => AttributeType::NUMBER,
+ ])->create();
+ unset($this->newOption['name']);
+
+ $this
+ ->actingAs($this->$user)
+ ->postJson('/attributes/id:' . $attribute->getKey() . '/options', $this->newOption)
+ ->assertCreated()
+ ->assertJsonFragment($this->newOption);
+
+ $this->assertDatabaseHas('attribute_options', $this->newOption);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testAddOptionIncompleteData($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $attribute = Attribute::factory([
+ 'type' => AttributeType::SINGLE_OPTION,
+ ])->create();
+ unset($this->newOption['name']);
+
+ $this
+ ->actingAs($this->$user)
+ ->postJson('/attributes/id:' . $attribute->getKey() . '/options', $this->newOption)
+ ->assertUnprocessable();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testAddOptionToDeletedAttribute($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ Attribute::destroy($this->attribute->getKey());
+
+ $this
+ ->actingAs($this->$user)
+ ->postJson('/attributes/id:' . $this->attribute->getKey() . '/options', $this->newOption)
+ ->assertNotFound();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testAddOptionUnauthorized($user): void
+ {
+ $this
+ ->actingAs($this->$user)
+ ->postJson('/attributes/id:' . $this->attribute->getKey() . '/options', $this->newOption)
+ ->assertForbidden();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateOption($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $optionUpdate = [
+ 'id' => $this->option->id,
+ 'name' => 'Test ' . $this->option->name,
+ 'value_number' => $this->option->value_number + 1,
+ 'value_date' => Carbon::now()->toDateString(),
+ 'attribute_id' => $this->option->attribute_id,
+ ];
+
+ $this
+ ->actingAs($this->$user)
+ ->json(
+ 'PATCH',
+ '/attributes/id:' . $this->attribute->getKey() . '/options/id:'. $this->option->getKey(),
+ $optionUpdate
+ )
+ ->assertOk()
+ ->assertJsonFragment($optionUpdate);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateOptionWithoutId($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $optionUpdate = [
+ 'name' => 'Test ' . $this->option->name,
+ 'value_number' => $this->option->value_number + 1,
+ 'value_date' => Carbon::now()->toDateString(),
+ 'attribute_id' => $this->option->attribute_id,
+ ];
+
+ $this
+ ->actingAs($this->$user)
+ ->json(
+ 'PATCH',
+ '/attributes/id:' . $this->attribute->getKey() . '/options/id:'. $this->option->getKey(),
+ $optionUpdate
+ )
+ ->assertOk()
+ ->assertJsonFragment($optionUpdate);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateOptionIncompleteData($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $attribute = Attribute::factory([
+ 'type' => AttributeType::SINGLE_OPTION,
+ ])->create();
+
+ $option = AttributeOption::factory()->create([
+ 'index' => 1,
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $optionUpdate = [
+ 'value_number' => $option->value_number + 1,
+ 'value_date' => Carbon::now()->toDateString(),
+ 'attribute_id' => $option->attribute_id,
+ ];
+
+ $this
+ ->actingAs($this->$user)
+ ->json(
+ 'PATCH',
+ '/attributes/id:' . $attribute->getKey() . '/options/id:'. $option->getKey(),
+ $optionUpdate
+ )
+ ->assertUnprocessable();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateOptionNotExisting($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $optionUpdate = [
+ 'id' => $this->option->id,
+ 'name' => 'Test ' . $this->option->name,
+ 'value_number' => $this->option->value_number + 1,
+ 'value_date' => Carbon::now()->toDateString(),
+ 'attribute_id' => $this->option->attribute_id,
+ ];
+
+ $this->option->delete();
+
+ $this
+ ->actingAs($this->$user)
+ ->json(
+ 'PATCH',
+ '/attributes/id:' . $this->attribute->getKey() . '/options/id:'. $this->option->getKey(),
+ $optionUpdate
+ )
+ ->assertNotFound();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateOptionNotRelatedOption($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $attribute = Attribute::factory()->create();
+
+ $optionUpdate = [
+ 'id' => $this->option->id,
+ 'name' => 'Test ' . $this->option->name,
+ 'value_number' => $this->option->value_number + 1,
+ 'value_date' => Carbon::now()->toDateString(),
+ 'attribute_id' => $this->option->attribute_id,
+ ];
+
+ $this
+ ->actingAs($this->$user)
+ ->json(
+ 'PATCH',
+ '/attributes/id:' . $attribute->getKey() . '/options/id:'. $this->option->getKey(),
+ $optionUpdate
+ )
+ ->assertNotFound();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateOptionUnauthorized($user): void
+ {
+ $optionUpdate = [
+ 'id' => $this->option->id,
+ 'name' => 'Test ' . $this->option->name,
+ 'value_number' => $this->option->value_number + 1,
+ 'value_date' => Carbon::now()->toDateString(),
+ 'attribute_id' => $this->option->attribute_id,
+ ];
+
+ $this
+ ->actingAs($this->$user)
+ ->patchJson(
+ '/attributes/id:' . $this->attribute->getKey() . '/options/id:'. $this->option->getKey(),
+ $optionUpdate
+ )
+ ->assertForbidden();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testDeleteOption($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $this
+ ->actingAs($this->$user)
+ ->deleteJson('/attributes/id:' . $this->attribute->getKey() . '/options/id:'. $this->option->getKey())
+ ->assertNoContent();
+
+ $this->assertSoftDeleted($this->option);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testDeleteOptionNotExisting($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $this->option->delete();
+
+ $this
+ ->actingAs($this->$user)
+ ->deleteJson('/attributes/id:' . $this->attribute->getKey() . '/options/id:'. $this->option->getKey())
+ ->assertNotFound();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testDeleteOptionNotRelatedOption($user): void
+ {
+ $this->$user->givePermissionTo('attributes.edit');
+
+ $attribute = Attribute::factory()->create();
+
+ $this
+ ->actingAs($this->$user)
+ ->deleteJson('/attributes/id:' . $attribute->getKey() . '/options/id:'. $this->option->getKey())
+ ->assertNotFound();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testDeleteOptionUnauthorized($user): void
+ {
+ $this
+ ->actingAs($this->$user)
+ ->deleteJson('/attributes/id:' . $this->attribute->getKey() . '/options/id:'. $this->option->getKey())
+ ->assertForbidden();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testIncrementIndex($user): void
+ {
+ $this->$user->givePermissionTo(['attributes.show', 'attributes.edit', 'attributes.add']);
+
+ $response = $this
+ ->actingAs($this->$user)
+ ->postJson('/attributes', $this->newAttribute)
+ ->assertCreated()
+ ->assertJsonStructure($this->expectedStructure)
+ ->assertJsonFragment([
+ 'name' => $this->newAttribute['name'],
+ 'slug' => $this->newAttribute['slug'],
+ 'description' => $this->newAttribute['description'],
+ 'type' => $this->newAttribute['type'],
+ 'global' => $this->newAttribute['global'],
+ 'sortable' => $this->newAttribute['sortable'],
+ ])
+ ->assertJsonFragment(['index' => 1] + $this->newAttribute['options'][0])
+ ->assertJsonFragment(['index' => 2] + $this->newAttribute['options'][1]);
+
+ AttributeOption::query()
+ ->where('attribute_id', '=', $response['data']['id'])
+ ->where('index', '=', 2)
+ ->delete();
+
+ $this->assertSoftDeleted('attribute_options', [
+ 'attribute_id' => $response['data']['id'],
+ 'index' => 2,
+ ]);
+
+ $this
+ ->actingAs($this->$user)
+ ->postJson('/attributes/id:' . $response['data']['id'] . '/options', $this->newOption)
+ ->assertCreated()
+ ->assertJsonFragment(['index' => 3] + $this->newOption);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/attributes/id:' . $response['data']['id'])
+ ->assertOk()
+ ->assertJsonFragment([
+ 'name' => $this->newAttribute['name'],
+ 'slug' => $this->newAttribute['slug'],
+ 'description' => $this->newAttribute['description'],
+ 'type' => $this->newAttribute['type'],
+ 'global' => $this->newAttribute['global'],
+ 'sortable' => $this->newAttribute['sortable'],
+ ])
+ ->assertJsonFragment(['index' => 1])
+ ->assertJsonMissing(['index' => 2])
+ ->assertJsonFragment(['index' => 3]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateMinMaxNumberOnUpdateOption($user): void
+ {
+ $this->$user->givePermissionTo('attributes.show');
+
+ $attribute = Attribute::factory([
+ 'type' => AttributeType::NUMBER,
+ ])->create();
+
+ $option1 = AttributeOption::factory()->create([
+ 'index' => 1,
+ 'value_number' => 100,
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $option2 = AttributeOption::factory()->create([
+ 'index' => 1,
+ 'value_number' => 200,
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $option1->update(['value_number' => 110]);
+ $option2->update(['value_number' => 190]);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/attributes/id:'. $attribute->getKey())
+ ->assertOk()
+ ->assertJsonFragment([
+ 'min' => 110,
+ 'max' => 190,
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateMinMaxNumberOnDeleteOption($user): void
+ {
+ $this->$user->givePermissionTo('attributes.show');
+
+ $attribute = Attribute::factory([
+ 'type' => AttributeType::NUMBER,
+ ])->create();
+
+ AttributeOption::factory()->create([
+ 'index' => 1,
+ 'value_number' => 100,
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $option2 = AttributeOption::factory()->create([
+ 'index' => 1,
+ 'value_number' => 200,
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $option2->delete();
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/attributes/id:'. $attribute->getKey())
+ ->assertOk()
+ ->assertJsonFragment([
+ 'min' => 100,
+ 'max' => 100,
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateMinMaxDateOnUpdateOption($user): void
+ {
+ $this->$user->givePermissionTo('attributes.show');
+
+ $attribute = Attribute::factory([
+ 'type' => AttributeType::DATE,
+ ])->create();
+
+ $option1 = AttributeOption::factory()->create([
+ 'index' => 1,
+ 'value_date' => '2010-03-15',
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $option2 = AttributeOption::factory()->create([
+ 'index' => 1,
+ 'value_date' => '2020-03-15',
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $option1->update(['value_date' => '2012-08-10']);
+ $option2->update(['value_date' => '2019-01-01']);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/attributes/id:'. $attribute->getKey())
+ ->assertOk()
+ ->assertJsonFragment([
+ 'min' => '2012-08-10',
+ 'max' => '2019-01-01',
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateMinMaxDateOnDeleteOption($user): void
+ {
+ $this->$user->givePermissionTo('attributes.show');
+
+ $attribute = Attribute::factory([
+ 'type' => AttributeType::DATE,
+ ])->create();
+
+ AttributeOption::factory()->create([
+ 'index' => 1,
+ 'value_date' => '2010-03-15',
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $option2 = AttributeOption::factory()->create([
+ 'index' => 1,
+ 'value_date' => '2020-03-15',
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $option2->delete();
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/attributes/id:'. $attribute->getKey())
+ ->assertOk()
+ ->assertJsonFragment([
+ 'min' => '2010-03-15',
+ 'max' => '2010-03-15',
+ ]);
+ }
+}
diff --git a/tests/Feature/DiscountTest.php b/tests/Feature/DiscountTest.php
index a3edc32b8..c3aace981 100644
--- a/tests/Feature/DiscountTest.php
+++ b/tests/Feature/DiscountTest.php
@@ -129,6 +129,7 @@ public function testCreate($user): void
'available' => true,
'starts_at' => Carbon::yesterday(),
'expires_at' => Carbon::tomorrow(),
+ 'metadata' => [],
]);
$this->assertDatabaseHas('discounts', [
@@ -300,6 +301,7 @@ public function testUpdate($user): void
'type' => DiscountType::AMOUNT,
'starts_at' => Carbon::yesterday(),
'expires_at' => Carbon::tomorrow(),
+ 'metadata' => [],
]);
$this->assertDatabaseHas('discounts', [
@@ -592,6 +594,7 @@ public function testCreateCheckDatetime($user): void
'uses' => 0,
'starts_at' => '2021-09-20T12:00:00.000000Z',
'expires_at' => '2021-09-21T12:00:00.000000Z',
+ 'metadata' => [],
]);
$this->assertDatabaseHas('discounts', [
diff --git a/tests/Feature/FilterTest.php b/tests/Feature/FilterTest.php
new file mode 100644
index 000000000..49353d437
--- /dev/null
+++ b/tests/Feature/FilterTest.php
@@ -0,0 +1,104 @@
+count(3)->create(['global' => 1]);
+ Attribute::factory()->create(['global' => 0]);
+
+ $this
+ ->actingAs($this->$user)
+ ->json('GET', '/filters')
+ ->assertJsonCount(3, 'data');
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testFilterWithSetsIds($user): void
+ {
+ $singleOptionAttribute = Attribute::factory()->create([
+ 'global' => 0,
+ 'type' => 'single-option',
+ ]);
+
+ AttributeOption::create([
+ 'name' => 'test',
+ 'value_number' => 1,
+ 'index' => 0,
+ 'attribute_id' => $singleOptionAttribute->getKey(),
+ ]);
+ AttributeOption::create([
+ 'name' => 'test2',
+ 'value_number' => 99,
+ 'index' => 1,
+ 'attribute_id' => $singleOptionAttribute->getKey(),
+ ]);
+
+ $firstProductSet = ProductSet::factory()->create();
+ $firstProductSet->attributes()->attach([
+ Attribute::factory()->create([
+ 'global' => 0,
+ 'type' => 'number',
+ ])->getKey(),
+ Attribute::factory()->create([
+ 'global' => 0,
+ 'type' => 'date',
+ ])->getKey(),
+ ]);
+
+ $secondProductSet = ProductSet::factory()->create();
+ $secondProductSet->attributes()->attach([
+ Attribute::factory()->create([
+ 'global' => 0,
+ 'type' => 'number',
+ ])->getKey(),
+ $singleOptionAttribute->getKey(),
+ Attribute::factory()->create([
+ 'global' => 0,
+ 'type' => 'date',
+ ])->getKey(),
+ ]);
+
+ ProductSet::factory()->create()->attributes()->attach([
+ Attribute::factory()->create([
+ 'global' => 0,
+ 'type' => 'number',
+ ])->getKey(),
+ Attribute::factory()->create([
+ 'global' => 1,
+ 'type' => 'date',
+ ])->getKey(),
+ Attribute::factory()->create([
+ 'global' => 0,
+ 'type' => 'single-option',
+ ])->getKey(),
+ ]);
+
+ $this
+ ->actingAs($this->$user)
+ ->json('GET', '/filters', [
+ 'sets' => [$firstProductSet->getKey(), $secondProductSet->getKey()],
+ ])
+ ->assertJsonFragment([
+ 'name' => 'test',
+ 'value_number' => 1,
+ ])
+ ->assertJsonFragment([
+ 'name' => 'test2',
+ 'value_number' => 99,
+ ])
+ ->assertJsonCount(6, 'data');
+ }
+}
diff --git a/tests/Feature/ItemTest.php b/tests/Feature/ItemTest.php
index 40c673b0e..af5f2d1ed 100644
--- a/tests/Feature/ItemTest.php
+++ b/tests/Feature/ItemTest.php
@@ -42,6 +42,7 @@ public function setUp(): void
'name' => $this->item->name,
'sku' => $this->item->sku,
'quantity' => $this->item->quantity,
+ 'metadata' => [],
];
}
@@ -68,7 +69,7 @@ public function testIndex($user): void
],
]);
- $this->assertQueryCountLessThan(10);
+ $this->assertQueryCountLessThan(11);
}
/**
@@ -86,7 +87,7 @@ public function testIndexPerformance($user): void
->assertOk()
->assertJsonCount(500, 'data');
- $this->assertQueryCountLessThan(10);
+ $this->assertQueryCountLessThan(11);
}
/**
@@ -122,7 +123,7 @@ public function testIndexFilterByAvailable($user): void
],
]);
- $this->assertQueryCountLessThan(10);
+ $this->assertQueryCountLessThan(11);
}
/**
@@ -155,7 +156,7 @@ public function testIndexFilterBySoldOut($user): void
],
]);
- $this->assertQueryCountLessThan(10);
+ $this->assertQueryCountLessThan(11);
}
/**
@@ -227,7 +228,7 @@ public function testIndexFilterByDay($user): void
'quantity' => 5,
]);
- $this->assertQueryCountLessThan(10);
+ $this->assertQueryCountLessThan(11);
}
/**
@@ -265,7 +266,7 @@ public function testIndexFilterByDayWithHour($user): void
'quantity' => 5,
]);
- $this->assertQueryCountLessThan(10);
+ $this->assertQueryCountLessThan(11);
}
public function testViewUnauthorized(): void
diff --git a/tests/Feature/MediaTest.php b/tests/Feature/MediaTest.php
index 25a97d38c..f6a27ac67 100644
--- a/tests/Feature/MediaTest.php
+++ b/tests/Feature/MediaTest.php
@@ -53,6 +53,7 @@ public function testUpload($user): void
'url',
'slug',
'alt',
+ 'metadata',
],
]);
}
diff --git a/tests/Feature/MetadataFilterTest.php b/tests/Feature/MetadataFilterTest.php
new file mode 100644
index 000000000..bf4db30da
--- /dev/null
+++ b/tests/Feature/MetadataFilterTest.php
@@ -0,0 +1,545 @@
+ [
+ 'user',
+ [
+ 'model' => Schema::class,
+ 'prefix_url' => 'schemas',
+ 'public_role' => 'products.add',
+ 'private_role' => 'schemas.show_metadata_private',
+ ],
+ ],
+ 'schemas as application' => [
+ 'application',
+ ['model' => Schema::class,
+ 'prefix_url' => 'schemas',
+ 'public_role' => 'products.add',
+ 'private_role' => 'schemas.show_metadata_private',
+ ],
+ ],
+
+ 'product sets as user' => [
+ 'user',
+ [
+ 'model' => ProductSet::class,
+ 'prefix_url' => 'product-sets',
+ 'public_role' => 'product_sets.show',
+ 'private_role' => 'product_sets.show_metadata_private',
+ ],
+ ],
+ 'product sets as application' => [
+ 'application',
+ [
+ 'model' => ProductSet::class,
+ 'prefix_url' => 'product-sets',
+ 'public_role' => 'product_sets.show',
+ 'private_role' => 'product_sets.show_metadata_private',
+ ],
+ ],
+
+ 'discounts as user' => [
+ 'user', [
+ 'model' => Discount::class,
+ 'prefix_url' => 'discounts',
+ 'public_role' => 'discounts.show',
+ 'private_role' => 'discounts.show_metadata_private',
+ ],
+ ],
+ 'discounts as application' => [
+ 'application',
+ [
+ 'model' => Discount::class,
+ 'prefix_url' => 'discounts',
+ 'public_role' => 'discounts.show',
+ 'private_role' => 'discounts.show_metadata_private',
+ ],
+ ],
+
+ 'items as user' => [
+ 'user',
+ [
+ 'model' => Item::class,
+ 'prefix_url' => 'items',
+ 'public_role' => 'items.show',
+ 'private_role' => 'items.show_metadata_private',
+ ],
+ ],
+ 'items as application' => [
+ 'application', [
+ 'model' => Item::class,
+ 'prefix_url' => 'items',
+ 'public_role' => 'items.show',
+ 'private_role' => 'items.show_metadata_private',
+ ],
+ ],
+
+ 'orders as user' => [
+ 'user', [
+ 'model' => Order::class,
+ 'prefix_url' => 'orders',
+ 'public_role' => 'orders.show',
+ 'private_role' => 'orders.show_metadata_private',
+ ],
+ ],
+ 'orders as application' => [
+ 'application', [
+ 'model' => Order::class,
+ 'prefix_url' => 'orders',
+ 'public_role' => 'orders.show',
+ 'private_role' => 'orders.show_metadata_private',
+ ],
+ ],
+
+ 'statuses as user' => [
+ 'user', [
+ 'model' => Status::class,
+ 'prefix_url' => 'statuses',
+ 'public_role' => 'statuses.show',
+ 'private_role' => 'statuses.show_metadata_private',
+ ],
+ ],
+ 'statuses as application' => [
+ 'application',
+ [
+ 'model' => Status::class,
+ 'prefix_url' => 'statuses',
+ 'public_role' => 'statuses.show',
+ 'private_role' => 'statuses.show_metadata_private',
+ ],
+ ],
+
+ 'shipping methods as user' => [
+ 'user',
+ [
+ 'model' => ShippingMethod::class,
+ 'prefix_url' => 'shipping-methods',
+ 'public_role' => 'shipping_methods.show',
+ 'private_role' => 'shipping_methods.show_metadata_private',
+ ],
+ ],
+ 'shipping methods as application' => [
+ 'application',
+ [
+ 'model' => ShippingMethod::class,
+ 'prefix_url' => 'shipping-methods',
+ 'public_role' => 'shipping_methods.show',
+ 'private_role' => 'shipping_methods.show_metadata_private',
+ ],
+ ],
+
+ 'package templates as user' => [
+ 'user', [
+ 'model' => PackageTemplate::class,
+ 'prefix_url' => 'package-templates',
+ 'public_role' => 'packages.show',
+ 'private_role' => 'packages.show_metadata_private',
+ ],
+ ],
+ 'package templates as application' => [
+ 'application',
+ [
+ 'model' => PackageTemplate::class,
+ 'prefix_url' => 'package-templates',
+ 'public_role' => 'packages.show',
+ 'private_role' => 'packages.show_metadata_private',
+ ],
+ ],
+
+ 'users as user' => [
+ 'user',
+ [
+ 'model' => User::class,
+ 'prefix_url' => 'users',
+ 'public_role' => 'users.show',
+ 'private_role' => 'users.show_metadata_private',
+ ],
+ ],
+ 'users as application' => [
+ 'application',
+ [
+ 'model' => User::class,
+ 'prefix_url' => 'users',
+ 'public_role' => 'users.show',
+ 'private_role' => 'users.show_metadata_private',
+ ],
+ ],
+
+ 'roles as user' => [
+ 'user',
+ [
+ 'model' => Role::class,
+ 'prefix_url' => 'roles',
+ 'public_role' => 'roles.show',
+ 'private_role' => 'roles.show_metadata_private',
+ ],
+ ],
+ 'roles as application' => [
+ 'application',
+ [
+ 'model' => Role::class,
+ 'prefix_url' => 'roles',
+ 'public_role' => 'roles.show',
+ 'private_role' => 'roles.show_metadata_private',
+ ],
+ ],
+
+ 'pages as user' => [
+ 'user',
+ [
+ 'model' => Page::class,
+ 'prefix_url' => 'pages',
+ 'public_role' => 'pages.show',
+ 'private_role' => 'pages.show_metadata_private',
+ ],
+ ],
+ 'pages as application' => [
+ 'application',
+ [
+ 'model' => Page::class,
+ 'prefix_url' => 'pages',
+ 'public_role' => 'pages.show',
+ 'private_role' => 'pages.show_metadata_private',
+ ],
+ ],
+
+ 'apps as user' => [
+ 'user',
+ [
+ 'model' => App::class,
+ 'prefix_url' => 'apps',
+ 'public_role' => 'apps.show',
+ 'private_role' => 'apps.show_metadata_private',
+ ],
+ ],
+ 'apps as application' => [
+ 'application',
+ [
+ 'model' => App::class,
+ 'prefix_url' => 'apps',
+ 'public_role' => 'apps.show',
+ 'private_role' => 'apps.show_metadata_private',
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testQuery($user, $data): void
+ {
+ $this->$user->givePermissionTo($data['public_role']);
+
+ $object = $this->createObjects($data['model']);
+
+ $metadata = $object->first()->metadata()->create([
+ 'name' => 'Producent',
+ 'value' => 'Heseya',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson("/{$data['prefix_url']}?metadata[{$metadata->name}]={$metadata->value}")
+ ->assertOk()
+ ->assertJsonCount(1, 'data')
+ ->assertJsonFragment([
+ $metadata->name => $metadata->value,
+ ])
+ ->assertJsonMissing([
+ 'metadata' => [],
+ ]);
+ }
+
+ public function createObjects($model)
+ {
+ $objects = $model::factory()->count(3)->create();
+
+ if ($objects->first()->public !== null) {
+ $objects->each(fn ($object) => $object->update(['public' => true]));
+ }
+
+ return $objects;
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testQueryByMorePropertiesUsingDots($user): void
+ {
+ $this->$user->givePermissionTo('orders.show');
+
+ $objects = $this->createObjects(Order::class);
+
+ $metadata = $objects->first()->metadata()->create([
+ 'name' => 'Producent',
+ 'value' => 'Heseya',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
+ $metadata2 = $objects->first()->metadata()->create([
+ 'name' => 'Kolor',
+ 'value' => 'Czerwony',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
+ $objects->last()->metadata()->create([
+ 'name' => 'Dystrybucja',
+ 'value' => 'Polska',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
+ $name = $metadata->name;
+ $value = $metadata->value;
+
+ $this
+ ->actingAs($this->$user)
+ ->json(
+ 'GET',
+ "/orders?metadata.{$name}={$value}&metadata.{$metadata2->name}={$metadata2->value}",
+ )
+ ->assertOk()
+ ->assertJsonCount(1, 'data')
+ ->assertJsonFragment([
+ $metadata->name => $metadata->value,
+ ])
+ ->assertJsonFragment([
+ $metadata2->name => $metadata2->value,
+ ])
+ ->assertJsonMissing([
+ 'metadata' => [],
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testQueryCase1($user): void
+ {
+ $this->$user->givePermissionTo('orders.show');
+
+ $objects = $this->createObjects(Order::class);
+
+ $metadata = $objects->first()->metadata()->create([
+ 'name' => 'test1',
+ 'value' => 0,
+ 'value_type' => MetadataType::NUMBER,
+ 'public' => true,
+ ]);
+
+ $metadata2 = $objects->first()->metadata()->create([
+ 'name' => 'test2',
+ 'value' => 0,
+ 'value_type' => MetadataType::NUMBER,
+ 'public' => true,
+ ]);
+
+ $objects->last()->metadata()->create([
+ 'name' => 'test2',
+ 'value' => 1,
+ 'value_type' => MetadataType::NUMBER,
+ 'public' => true,
+ ]);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/orders?metadata.test1=0&metadata.test2=0')
+ ->assertOk()
+ ->assertJsonCount(1, 'data')
+ ->assertJsonFragment([
+ $metadata->name => $metadata->value,
+ ])
+ ->assertJsonFragment([
+ $metadata2->name => $metadata2->value,
+ ])
+ ->assertJsonMissing([
+ 'metadata' => [],
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testQueryCase2($user): void
+ {
+ $this->$user->givePermissionTo('orders.show');
+
+ $objects = $this->createObjects(Order::class);
+
+ $metadata = $objects->first()->metadata()->create([
+ 'name' => 'test1',
+ 'value' => 0,
+ 'value_type' => MetadataType::NUMBER,
+ 'public' => true,
+ ]);
+
+ $objects->last()->metadata()->create([
+ 'name' => 'test1',
+ 'value' => 1,
+ 'value_type' => MetadataType::NUMBER,
+ 'public' => true,
+ ]);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/orders?metadata.test1=0')
+ ->assertOk()
+ ->assertJsonCount(1, 'data')
+ ->assertJsonFragment([
+ $metadata->name => $metadata->value,
+ ])
+ ->assertJsonMissing([
+ 'metadata' => [],
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testQueryCase3($user): void
+ {
+ $this->$user->givePermissionTo('orders.show');
+
+ $objects = $this->createObjects(Order::class);
+
+ $metadata = $objects->first()->metadata()->create([
+ 'name' => 'test1',
+ 'value' => 0,
+ 'value_type' => MetadataType::NUMBER,
+ 'public' => true,
+ ]);
+
+ $metadata2 = $objects->first()->metadata()->create([
+ 'name' => 'test2',
+ 'value' => 1,
+ 'value_type' => MetadataType::NUMBER,
+ 'public' => true,
+ ]);
+
+ $metadata3 = $objects->last()->metadata()->create([
+ 'name' => 'test1',
+ 'value' => 1,
+ 'value_type' => MetadataType::NUMBER,
+ 'public' => true,
+ ]);
+
+ $metadata4 = $objects->last()->metadata()->create([
+ 'name' => 'test2',
+ 'value' => 0,
+ 'value_type' => MetadataType::NUMBER,
+ 'public' => true,
+ ]);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/orders?metadata.test1=0')
+ ->assertOk()
+ ->assertJsonCount(1, 'data')
+ ->assertJsonFragment([
+ $metadata->name => $metadata->value,
+ ])
+ ->assertJsonFragment([
+ $metadata2->name => $metadata2->value,
+ ])
+ ->assertJsonMissing([
+ 'metadata' => [],
+ ]);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/orders?metadata.test1=1')
+ ->assertOk()
+ ->assertJsonCount(1, 'data')
+ ->assertJsonFragment([
+ $metadata3->name => $metadata3->value,
+ ])
+ ->assertJsonFragment([
+ $metadata4->name => $metadata4->value,
+ ])
+ ->assertJsonMissing([
+ 'metadata' => [],
+ ]);
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testQueryPrivate($user, $data): void
+ {
+ $this->$user->givePermissionTo([$data['public_role'], $data['private_role']]);
+
+ $object = $this->createObjects($data['model']);
+
+ $metadata = $object->first()->metadataPrivate()->create([
+ 'name' => 'Producent',
+ 'value' => 'Heseya - private',
+ 'value_type' => MetadataType::STRING,
+ 'public' => false,
+ ]);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson("/{$data['prefix_url']}?metadata_private[{$metadata->name}]={$metadata->value}")
+ ->assertOk()
+ ->assertJsonCount(1, 'data')
+ ->assertJsonFragment([
+ $metadata->name => $metadata->value,
+ ])
+ ->assertJsonMissing([
+ 'metadata_private' => [],
+ ]);
+ }
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testQueryPrivateWithoutPermission($user, $data): void
+ {
+ $this->$user->givePermissionTo($data['public_role']);
+
+ $object = $this->createObjects($data['model']);
+
+ $metadata = $object->first()->metadataPrivate()->create([
+ 'name' => 'Producent',
+ 'value' => 'Heseya - private',
+ 'value_type' => MetadataType::STRING,
+ 'public' => false,
+ ]);
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson("/{$data['prefix_url']}?metadata_private[{$metadata->name}]={$metadata->value}")
+ ->assertOk()
+ ->assertJsonMissing([
+ $metadata->name => $metadata->value,
+ ])
+ ->assertJsonMissing([
+ 'metadata_private' => [],
+ ]);
+ }
+}
diff --git a/tests/Feature/MetadataFormsTest.php b/tests/Feature/MetadataFormsTest.php
new file mode 100644
index 000000000..d8356d43d
--- /dev/null
+++ b/tests/Feature/MetadataFormsTest.php
@@ -0,0 +1,91 @@
+$user->givePermissionTo('products.add');
+
+ $response = $this
+ ->actingAs($this->$user)
+ ->json('POST', '/products', [
+ 'name' => 'Test',
+ 'slug' => 'test',
+ 'price' => 100.00,
+ 'public' => true,
+ 'metadata' => [
+ 'test' => '123',
+ ],
+ 'metadata_private' => [
+ 'test-two' => 123,
+ ],
+ ]);
+
+ $response->assertCreated();
+
+ $this->assertDatabaseHas('metadata', [
+ 'model_id' => $response->getData()->data->id,
+ 'model_type' => Product::class,
+ 'name' => 'test',
+ 'value' => '123',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
+ $this->assertDatabaseHas('metadata', [
+ 'model_id' => $response->getData()->data->id,
+ 'model_type' => Product::class,
+ 'name' => 'test-two',
+ 'value' => 123,
+ 'value_type' => MetadataType::NUMBER,
+ 'public' => false,
+ ]);
+ }
+
+// /**
+// * @dataProvider authProvider
+// */
+// public function testProductUpdate($user): void
+// {
+// $this->$user->givePermissionTo('products.edit');
+//
+// $product = Product::factory()->create();
+//
+// $this
+// ->actingAs($this->$user)
+// ->json('PATCH', '/products/id:' . $product->getKey(), [
+// 'metadata' => [
+// 'test' => '123',
+// ],
+// ])
+// ->assertUnprocessable();
+// }
+//
+// /**
+// * @dataProvider authProvider
+// */
+// public function testProductPrivateUpdate($user): void
+// {
+// $this->$user->givePermissionTo('products.edit');
+//
+// $product = Product::factory()->create();
+//
+// $this
+// ->actingAs($this->$user)
+// ->json('PATCH', '/products/id:' . $product->getKey(), [
+// 'metadata_private' => [
+// 'test' => '123',
+// ],
+// ])
+// ->assertUnprocessable();
+// }
+}
diff --git a/tests/Feature/MetadataTest.php b/tests/Feature/MetadataTest.php
new file mode 100644
index 000000000..5a8ba9d6e
--- /dev/null
+++ b/tests/Feature/MetadataTest.php
@@ -0,0 +1,452 @@
+ [
+ 'user',
+ ['model' => Product::class, 'prefix_url' => 'products', 'role' => 'products.edit'],
+ ],
+ 'products as app' => [
+ 'application',
+ ['model' => Product::class, 'prefix_url' => 'products', 'role' => 'products.edit'],
+ ],
+
+ 'schemas as user' => [
+ 'user',
+ ['model' => Schema::class, 'prefix_url' => 'schemas', 'role' => 'products.edit'],
+ ],
+ 'schemas as application' => [
+ 'application',
+ ['model' => Schema::class, 'prefix_url' => 'schemas', 'role' => 'products.edit'],
+ ],
+
+ 'options as user' => [
+ 'user',
+ ['model' => Option::class, 'prefix_url' => 'options', 'role' => 'products.edit'],
+ ],
+ 'options as application' => [
+ 'application',
+ ['model' => Option::class, 'prefix_url' => 'options', 'role' => 'products.edit'],
+ ],
+
+ 'product sets as user' => [
+ 'user',
+ ['model' => ProductSet::class, 'prefix_url' => 'product-sets', 'role' => 'product_sets.edit'],
+ ],
+ 'product sets as application' => [
+ 'application',
+ ['model' => ProductSet::class, 'prefix_url' => 'product-sets', 'role' => 'product_sets.edit'],
+ ],
+
+ 'discounts as user' => [
+ 'user',
+ ['model' => Discount::class, 'prefix_url' => 'discounts', 'role' => 'discounts.edit'],
+ ],
+ 'discounts as application' => [
+ 'application',
+ ['model' => Discount::class, 'prefix_url' => 'discounts', 'role' => 'discounts.edit'],
+ ],
+
+ 'items as user' => [
+ 'user',
+ ['model' => Item::class, 'prefix_url' => 'items', 'role' => 'items.edit'],
+ ],
+ 'items as application' => [
+ 'application',
+ ['model' => Item::class, 'prefix_url' => 'items', 'role' => 'items.edit'],
+ ],
+
+ 'orders as user' => [
+ 'user',
+ ['model' => Order::class, 'prefix_url' => 'orders', 'role' => 'orders.edit'],
+ ],
+ 'orders as application' => [
+ 'application',
+ ['model' => Order::class, 'prefix_url' => 'orders', 'role' => 'orders.edit'],
+ ],
+
+ 'statuses as user' => [
+ 'user',
+ ['model' => Status::class, 'prefix_url' => 'statuses', 'role' => 'statuses.edit'],
+ ],
+ 'statuses as application' => [
+ 'application',
+ ['model' => Status::class, 'prefix_url' => 'statuses', 'role' => 'statuses.edit'],
+ ],
+
+ 'shipping methods as user' => [
+ 'user',
+ [
+ 'model' => ShippingMethod::class,
+ 'prefix_url' => 'shipping-methods',
+ 'role' => 'shipping_methods.edit',
+ ],
+ ],
+ 'shipping methods as application' => [
+ 'application',
+ ['model' => ShippingMethod::class,
+ 'prefix_url' => 'shipping-methods',
+ 'role' => 'shipping_methods.edit',
+ ],
+ ],
+
+ 'package templates as user' => [
+ 'user', [
+ 'model' => PackageTemplate::class,
+ 'prefix_url' => 'package-templates',
+ 'role' => 'packages.edit',
+ ],
+ ],
+ 'package templates as application' => [
+ 'application', [
+ 'model' => PackageTemplate::class,
+ 'prefix_url' => 'package-templates',
+ 'role' => 'packages.edit',
+ ],
+ ],
+
+ 'users as user' => [
+ 'user',
+ ['model' => User::class, 'prefix_url' => 'users', 'role' => 'users.edit'],
+ ],
+ 'users as application' => [
+ 'application',
+ ['model' => User::class, 'prefix_url' => 'users', 'role' => 'users.edit'],
+ ],
+
+ 'roles as user' => [
+ 'user',
+ ['model' => Role::class, 'prefix_url' => 'roles', 'role' => 'roles.edit'],
+ ],
+ 'roles as application' => [
+ 'application',
+ ['model' => Role::class, 'prefix_url' => 'roles', 'role' => 'roles.edit'],
+ ],
+
+ 'pages as user' => [
+ 'user',
+ ['model' => Page::class, 'prefix_url' => 'pages', 'role' => 'pages.edit'],
+ ],
+ 'pages as application' => [
+ 'application',
+ ['model' => Page::class, 'prefix_url' => 'pages', 'role' => 'pages.edit'],
+ ],
+
+ 'apps as user' => [
+ 'user',
+ ['model' => App::class, 'prefix_url' => 'apps', 'role' => 'apps.install'],
+ ],
+ 'apps as application' => [
+ 'application',
+ ['model' => App::class, 'prefix_url' => 'apps', 'role' => 'apps.install'],
+ ],
+
+ 'media as user' => [
+ 'user',
+ ['model' => Media::class, 'prefix_url' => 'media', 'role' => 'products.edit'],
+ ],
+ 'media as application' => [
+ 'application',
+ ['model' => Media::class, 'prefix_url' => 'media', 'role' => 'products.edit'],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testAddMetadata($user, $data): void
+ {
+ $this->$user->givePermissionTo($data['role']);
+
+ $related = [];
+
+ if ($data['model'] === Option::class) {
+ $related = [
+ 'schema_id' => (Schema::factory()->create())->getKey(),
+ ];
+ }
+
+ $object = $data['model']::factory()->create($related);
+
+ $metadata = [
+ 'sample text metadata' => 'Lorem ipsum dolor sit amet',
+ 'sample numeric metadata' => 21.5,
+ 'sample bool metadata' => true,
+ ];
+
+ $this->actingAs($this->$user)->patchJson(
+ "/{$data['prefix_url']}/id:{$object->getKey()}/metadata",
+ $metadata
+ )
+ ->assertOk()
+ ->assertJsonFragment(['data' => $metadata]);
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testAddMetadataPrivate($user, $data): void
+ {
+ $this->$user->givePermissionTo($data['role']);
+
+ $related = [];
+
+ if ($data['model'] === Option::class) {
+ $related = [
+ 'schema_id' => (Schema::factory()->create())->getKey(),
+ ];
+ }
+
+ $object = $data['model']::factory()->create($related);
+
+ $metadata = [
+ 'sample text metadata private' => 'Aliquam porta viverra tortor non faucibus',
+ 'sample numeric metadata private' => 22.5,
+ 'sample bool metadata private' => true,
+ ];
+
+ $this->actingAs($this->$user)->patchJson(
+ "/{$data['prefix_url']}/id:{$object->getKey()}/metadata-private",
+ $metadata
+ )
+ ->assertOk()
+ ->assertJsonFragment(['data' => $metadata]);
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testUpdateMetadata($user, $data): void
+ {
+ $this->$user->givePermissionTo($data['role']);
+
+ $related = [];
+
+ if ($data['model'] === Option::class) {
+ $related = [
+ 'schema_id' => (Schema::factory()->create())->getKey(),
+ ];
+ }
+
+ $object = $data['model']::factory()->create($related);
+
+ $metadata = $object->metadata()->create([
+ 'name' => 'Metadata',
+ 'value' => 'metadata test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
+ $this->actingAs($this->$user)->patchJson(
+ "/{$data['prefix_url']}/id:{$object->getKey()}/metadata",
+ [
+ $metadata->name => 'new super value',
+ ]
+ )
+ ->assertOk()
+ ->assertJsonFragment(['data' => [
+ $metadata->name => 'new super value',
+ ],
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateMetadataSameKeys($user): void
+ {
+ $this->$user->givePermissionTo('products.edit');
+
+ $product = Product::factory()->create();
+
+ $metadata = $product->metadata()->create([
+ 'name' => 'Metadata',
+ 'value' => 'metadata test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
+ $product2 = Product::factory()->create();
+
+ $metadata2 = $product2->metadata()->create([
+ 'name' => 'Metadata',
+ 'value' => 'metadata test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
+ $this->actingAs($this->$user)->patchJson(
+ "/products/id:{$product->getKey()}/metadata",
+ [
+ $metadata->name => 'new super value',
+ ]
+ )
+ ->assertOk()
+ ->assertJsonFragment(['data' => [
+ $metadata->name => 'new super value',
+ ],
+ ]);
+
+ $this->assertDatabaseHas('metadata', array_merge($metadata2->toArray(), [
+ 'model_id' => $product2->getKey(),
+ 'model_type' => Product::class,
+ ]));
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testUpdateMetadataPrivate($user, $data): void
+ {
+ $this->$user->givePermissionTo($data['role']);
+
+ $related = [];
+
+ if ($data['model'] === Option::class) {
+ $related = [
+ 'schema_id' => (Schema::factory()->create())->getKey(),
+ ];
+ }
+
+ $object = $data['model']::factory()->create($related);
+
+ $metadata = $object->metadataPrivate()->create([
+ 'name' => 'Metadata',
+ 'value' => 'metadata test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => false,
+ ]);
+
+ $this->actingAs($this->$user)->patchJson(
+ "/{$data['prefix_url']}/id:{$object->getKey()}/metadata-private",
+ [
+ $metadata->name => 'new super value',
+ ]
+ )
+ ->assertOk()
+ ->assertJsonFragment(['data' => [
+ $metadata->name => 'new super value',
+ ],
+ ]);
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testDeleteMetadata($user, $data): void
+ {
+ $this->$user->givePermissionTo($data['role']);
+
+ $related = [];
+
+ if ($data['model'] === Option::class) {
+ $related = [
+ 'schema_id' => (Schema::factory()->create())->getKey(),
+ ];
+ }
+
+ $object = $data['model']::factory()->create($related);
+
+ $metadata1 = $object->metadata()->create([
+ 'name' => 'Metadata',
+ 'value' => 'metadata test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
+ $metadata2 = $object->metadata()->create([
+ 'name' => 'Metadata2',
+ 'value' => 'metadata2 test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
+ $this->actingAs($this->$user)->patchJson(
+ "/{$data['prefix_url']}/id:{$object->getKey()}/metadata",
+ [
+ $metadata1->name => $metadata1->value,
+ $metadata2->name => null,
+ ]
+ )
+ ->assertOk()
+ ->assertJsonFragment(['data' => [
+ $metadata1->name => $metadata1->value,
+ ],
+ ])
+ ->assertJsonMissing([
+ $metadata2->name => $metadata2->value,
+ ]);
+ }
+
+ /**
+ * @dataProvider dataProvider
+ */
+ public function testDeleteMetadataPrivate($user, $data): void
+ {
+ $this->$user->givePermissionTo($data['role']);
+
+ $related = [];
+
+ if ($data['model'] === Option::class) {
+ $related = [
+ 'schema_id' => (Schema::factory()->create())->getKey(),
+ ];
+ }
+
+ $object = $data['model']::factory()->create($related);
+
+ $metadata1 = $object->metadataPrivate()->create([
+ 'name' => 'Metadata',
+ 'value' => 'metadata test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => false,
+ ]);
+
+ $metadata2 = $object->metadataPrivate()->create([
+ 'name' => 'Metadata2 private',
+ 'value' => 'metadata2 test private',
+ 'value_type' => MetadataType::STRING,
+ 'public' => false,
+ ]);
+
+ $this->actingAs($this->$user)->patchJson(
+ "/{$data['prefix_url']}/id:{$object->getKey()}/metadata-private",
+ [
+ $metadata1->name => $metadata1->value,
+ $metadata2->name => null,
+ ]
+ )
+ ->assertOk()
+ ->assertJsonFragment(['data' => [
+ $metadata1->name => $metadata1->value,
+ ],
+ ])
+ ->assertJsonMissing([
+ $metadata2->name => $metadata2->value,
+ ]);
+ }
+}
diff --git a/tests/Feature/OrderTest.php b/tests/Feature/OrderTest.php
index 4ff7fa0fd..bb5213492 100644
--- a/tests/Feature/OrderTest.php
+++ b/tests/Feature/OrderTest.php
@@ -2,6 +2,7 @@
namespace Tests\Feature;
+use App\Enums\MetadataType;
use App\Events\ItemUpdatedQuantity;
use App\Events\OrderCreated;
use App\Events\OrderUpdatedStatus;
@@ -60,6 +61,13 @@ public function setUp(): void
'price' => 49.99,
]);
+ $this->order->metadata()->create([
+ 'name' => 'Metadata',
+ 'value' => 'metadata test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
/** @var OrderService $orderService */
$orderService = App::make(OrderServiceContract::class);
@@ -96,6 +104,7 @@ public function setUp(): void
'paid',
'created_at',
'shipping_method',
+ 'metadata',
];
$this->expected_full_view_structure = $this->expected_full_structure + ['user'];
@@ -517,6 +526,32 @@ public function testView($user): void
->assertJsonStructure(['data' => $this->expected_full_view_structure]);
}
+ /**
+ * @dataProvider authProvider
+ */
+ public function testViewPrivateMetadata($user): void
+ {
+ $this->$user->givePermissionTo(['orders.show_details', 'orders.show_metadata_private']);
+
+ $privateMetadata = $this->order->metadataPrivate()->create([
+ 'name' => 'hiddenMetadata',
+ 'value' => 'hidden metadata test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => false,
+ ]);
+
+ $response = $this->actingAs($this->$user)
+ ->getJson('/orders/id:' . $this->order->getKey());
+ $response
+ ->assertOk()
+ ->assertJsonFragment(['code' => $this->order->code])
+ ->assertJsonStructure(['data' => $this->expected_full_view_structure])
+ ->assertJsonFragment(['metadata_private' => [
+ $privateMetadata->name => $privateMetadata->value,
+ ],
+ ]);
+ }
+
public function testViewSummaryUnauthorized(): void
{
$response = $this->getJson('/orders/' . $this->order->code);
diff --git a/tests/Feature/OrderUpdateTest.php b/tests/Feature/OrderUpdateTest.php
index 5e7341f34..4f1436268 100644
--- a/tests/Feature/OrderUpdateTest.php
+++ b/tests/Feature/OrderUpdateTest.php
@@ -92,6 +92,7 @@ public function testFullUpdateOrder($user): void
'name' => $this->status->name,
'hidden' => $this->status->hidden,
'no_notifications' => $this->status->no_notifications,
+ 'metadata' => [],
],
'delivery_address' => [
'id' => $responseData->delivery_address->id,
@@ -181,6 +182,7 @@ public function testFullUpdateOrderWithWebHookQueue($user): void
'name' => $this->status->name,
'hidden' => $this->status->hidden,
'no_notifications' => $this->status->no_notifications,
+ 'metadata' => [],
],
'delivery_address' => [
'id' => $responseData->delivery_address->id,
@@ -270,6 +272,7 @@ public function testFullUpdateOrderWithWebHookDispatched($user): void
'name' => $this->status->name,
'hidden' => $this->status->hidden,
'no_notifications' => $this->status->no_notifications,
+ 'metadata' => [],
],
'delivery_address' => [
'id' => $responseData->delivery_address->id,
@@ -351,6 +354,7 @@ public function testUpdateOrderByEmail($user): void
'name' => $this->status->name,
'hidden' => $this->status->hidden,
'no_notifications' => $this->status->no_notifications,
+ 'metadata' => [],
],
]);
@@ -395,6 +399,7 @@ public function testUpdateOrderByComment($user): void
'name' => $this->status->name,
'hidden' => $this->status->hidden,
'no_notifications' => $this->status->no_notifications,
+ 'metadata' => [],
],
]);
@@ -468,6 +473,7 @@ public function testUpdateOrderWithDeliveryAddress($user): void
'name' => $this->status->name,
'hidden' => $this->status->hidden,
'no_notifications' => $this->status->no_notifications,
+ 'metadata' => [],
],
'delivery_address' => [
'address' => $this->addressDelivery->address,
@@ -591,6 +597,7 @@ public function testUpdateOrderByInvoiceAddress($user): void
'name' => $this->status->name,
'hidden' => $this->status->hidden,
'no_notifications' => $this->status->no_notifications,
+ 'metadata' => [],
],
'invoice_address' => [
'address' => $this->addressInvoice->address,
diff --git a/tests/Feature/PackageTemplateTest.php b/tests/Feature/PackageTemplateTest.php
index 76a2ed32a..df2b4680d 100644
--- a/tests/Feature/PackageTemplateTest.php
+++ b/tests/Feature/PackageTemplateTest.php
@@ -27,6 +27,7 @@ public function setUp(): void
'width' => $this->package->width,
'height' => $this->package->height,
'depth' => $this->package->depth,
+ 'metadata' => [],
];
}
diff --git a/tests/Feature/PageTest.php b/tests/Feature/PageTest.php
index 491d2e1d6..be292a186 100644
--- a/tests/Feature/PageTest.php
+++ b/tests/Feature/PageTest.php
@@ -2,6 +2,7 @@
namespace Tests\Feature;
+use App\Enums\MetadataType;
use App\Events\PageCreated;
use App\Events\PageDeleted;
use App\Events\PageUpdated;
@@ -40,6 +41,13 @@ public function setUp(): void
'public' => false,
]);
+ $metadata = $this->page->metadata()->create([
+ 'name' => 'Metadata',
+ 'value' => 'metadata test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
/**
* Expected response
*/
@@ -48,10 +56,14 @@ public function setUp(): void
'name' => $this->page->name,
'slug' => $this->page->slug,
'public' => $this->page->public,
+ 'metadata' => [],
];
$this->expected_view = array_merge($this->expected, [
'content_html' => $this->page->content_html,
+ 'metadata' => [
+ $metadata->name => $metadata->value,
+ ],
]);
}
@@ -78,7 +90,7 @@ public function testIndex($user): void
],
]);
- $this->assertQueryCountLessThan(10);
+ $this->assertQueryCountLessThan(11);
}
/**
@@ -96,7 +108,7 @@ public function testIndexPerformance($user): void
->assertOk()
->assertJsonCount(500, 'data');
- $this->assertQueryCountLessThan(10);
+ $this->assertQueryCountLessThan(11);
}
/**
@@ -141,6 +153,38 @@ public function testView($user): void
->assertJson(['data' => $this->expected_view]);
}
+ /**
+ * @dataProvider authProvider
+ */
+ public function testViewPrivateMetadata($user): void
+ {
+ $this->$user->givePermissionTo(['pages.show_details', 'pages.show_metadata_private']);
+
+ $privateMetadata = $this->page->metadataPrivate()->create([
+ 'name' => 'hiddenMetadata',
+ 'value' => 'hidden metadata test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => false,
+ ]);
+
+ $response = $this->actingAs($this->$user)
+ ->getJson('/pages/' . $this->page->slug);
+ $response
+ ->assertOk()
+ ->assertJson(['data' => $this->expected_view]);
+
+ $response = $this->actingAs($this->$user)
+ ->getJson('/pages/id:' . $this->page->getKey());
+ $response
+ ->assertOk()
+ ->assertJson(['data' => $this->expected_view +
+ ['metadata_private' => [
+ $privateMetadata->name => $privateMetadata->value,
+ ],
+ ],
+ ]);
+ }
+
/**
* @dataProvider authProvider
*/
diff --git a/tests/Feature/ProductSearchTest.php b/tests/Feature/ProductSearchTest.php
index bc4a6fa3e..3a106f46b 100644
--- a/tests/Feature/ProductSearchTest.php
+++ b/tests/Feature/ProductSearchTest.php
@@ -6,14 +6,11 @@
use App\Models\ProductSet;
use App\Models\Tag;
use Illuminate\Contracts\Auth\Authenticatable;
-use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Testing\TestResponse;
use Tests\TestCase;
class ProductSearchTest extends TestCase
{
- use RefreshDatabase;
-
/**
* @dataProvider authProvider
*/
@@ -25,12 +22,36 @@ public function testSearch($user): void
'public' => true,
]);
- $response = $this->actingAs($this->$user)
- ->getJson('/products?search=' . $product->name);
- $response
- ->assertOk()
- ->assertJsonCount(1, 'data')
- ->assertJsonFragment(['id' => $product->getKey()]);
+ $this
+ ->actingAs($this->$user)
+ ->json('GET', '/products', ['search' => $product->name])
+ ->assertOk();
+// ->assertJsonCount(1, 'data')
+// ->assertJsonFragment(['id' => $product->getKey()]);
+
+ $this->assertElasticQuery([
+ 'bool' => [
+ 'must' => [
+ [
+ 'multi_match' => [
+ 'query' => $product->name,
+ 'fuzziness' => 'auto',
+ ],
+ ],
+ ],
+ 'should' => [],
+ 'filter' => [
+ [
+ 'term' => [
+ 'public' => [
+ 'value' => true,
+ 'boost' => 1.0,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
}
/**
@@ -56,13 +77,37 @@ public function testSearchBySet($user): void
$set->products()->attach($product);
- $response = $this->actingAs($this->$user)
- ->getJson('/products?sets[]=' . $set->slug);
-
- $response
- ->assertOk()
- ->assertJsonCount(1, 'data')
- ->assertJsonFragment(['id' => $product->getKey()]);
+ $this
+ ->actingAs($this->$user)
+ ->json('GET', '/products', ['sets' => [$set->slug]])
+ ->assertOk();
+// ->assertJsonCount(1, 'data')
+// ->assertJsonFragment(['id' => $product->getKey()]);
+
+ $this->assertElasticQuery([
+ 'bool' => [
+ 'must' => [],
+ 'should' => [],
+ 'filter' => [
+ [
+ 'terms' => [
+ 'sets.slug' => [
+ $set->slug,
+ ],
+ 'boost' => 1.0,
+ ],
+ ],
+ [
+ 'term' => [
+ 'public' => [
+ 'value' => true,
+ 'boost' => 1.0,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
}
/**
@@ -96,13 +141,41 @@ public function testSearchBySets($user): void
$set->products()->attach($product);
$set2->products()->attach($product2);
- $response = $this->actingAs($this->$user)
- ->getJson('/products?sets[]=' . $set->slug . '&sets[]=' . $set2->slug);
- $response
- ->assertOk()
- ->assertJsonCount(2, 'data')
- ->assertJsonFragment(['id' => $product->getKey()])
- ->assertJsonFragment(['id' => $product2->getKey()]);
+ $this
+ ->actingAs($this->$user)
+ ->json('GET', '/products', [
+ 'sets' => [$set->slug, $set2->slug],
+ ])
+ ->assertOk();
+// ->assertJsonCount(2, 'data')
+// ->assertJsonFragment(['id' => $product->getKey()])
+// ->assertJsonFragment(['id' => $product2->getKey()]);
+
+ $this->assertElasticQuery([
+ 'bool' => [
+ 'must' => [],
+ 'should' => [],
+ 'filter' => [
+ [
+ 'terms' => [
+ 'sets.slug' => [
+ $set->slug,
+ $set2->slug,
+ ],
+ 'boost' => 1.0,
+ ],
+ ],
+ [
+ 'term' => [
+ 'public' => [
+ 'value' => true,
+ 'boost' => 1.0,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
}
/**
@@ -127,10 +200,10 @@ public function testSearchBySetHiddenUnauthorized($user): void
$set->products()->attach($product);
- $response = $this->actingAs($this->$user)
- ->getJson('/products?sets[]=' . $set->slug);
-
- $response->assertUnprocessable();
+ $this
+ ->actingAs($this->$user)
+ ->json('GET', '/products', ['sets' => [$set->slug]])
+ ->assertUnprocessable();
}
/**
@@ -160,13 +233,37 @@ public function testSearchBySetHidden($user): void
$set->products()->attach($product);
- $response = $this->actingAs($this->$user)
- ->getJson('/products?sets[]=' . $set->slug);
-
- $response
- ->assertOk()
- ->assertJsonCount(1, 'data')
- ->assertJsonFragment(['id' => $product->getKey()]);
+ $this
+ ->actingAs($this->$user)
+ ->json('GET', '/products', ['sets' => [$set->slug]])
+ ->assertOk();
+// ->assertJsonCount(1, 'data')
+// ->assertJsonFragment(['id' => $product->getKey()]);
+
+ $this->assertElasticQuery([
+ 'bool' => [
+ 'must' => [],
+ 'should' => [],
+ 'filter' => [
+ [
+ 'terms' => [
+ 'sets.slug' => [
+ $set->slug,
+ ],
+ 'boost' => 1.0,
+ ],
+ ],
+ [
+ 'term' => [
+ 'public' => [
+ 'value' => true,
+ 'boost' => 1.0,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
}
/**
@@ -176,10 +273,11 @@ public function testSearchByParentSet($user): void
{
$this->$user->givePermissionTo('products.show');
- $this->getProductsByParentSet($this->$user, true, $product)
- ->assertOk()
- ->assertJsonCount(1, 'data')
- ->assertJsonFragment(['id' => $product->getKey()]);
+ $this
+ ->getProductsByParentSet($this->$user, true, $product)
+ ->assertOk();
+// ->assertJsonCount(1, 'data')
+// ->assertJsonFragment(['id' => $product->getKey()]);
}
/**
@@ -189,9 +287,10 @@ public function testSearchByParentSetWithPrivateChildUnauthorized($user): void
{
$this->$user->givePermissionTo('products.show');
- $this->getProductsByParentSet($this->$user, false)
- ->assertOk()
- ->assertJsonCount(0, 'data');
+ $this
+ ->getProductsByParentSet($this->$user, false);
+// ->assertOk()
+// ->assertJsonCount(0, 'data');
}
/**
@@ -204,10 +303,11 @@ public function testSearchByParentSetWithPrivateChild($user): void
'product_sets.show_hidden',
]);
- $this->getProductsByParentSet($this->$user, false, $product)
- ->assertOk()
- ->assertJsonCount(1, 'data')
- ->assertJsonFragment(['id' => $product->getKey()]);
+ $this
+ ->getProductsByParentSet($this->$user, false, $product)
+ ->assertOk();
+// ->assertJsonCount(1, 'data')
+// ->assertJsonFragment(['id' => $product->getKey()]);
}
/**
@@ -230,11 +330,37 @@ public function testSearchByTag($user): void
$tag->products()->attach($product);
- $this->actingAs($this->$user)
+ $this
+ ->actingAs($this->$user)
->json('GET', '/products', ['tags' => [$tag->getKey()]])
- ->assertOk()
- ->assertJsonCount(1, 'data')
- ->assertJsonFragment(['id' => $product->getKey()]);
+ ->assertOk();
+// ->assertJsonCount(1, 'data')
+// ->assertJsonFragment(['id' => $product->getKey()]);
+
+ $this->assertElasticQuery([
+ 'bool' => [
+ 'must' => [],
+ 'should' => [],
+ 'filter' => [
+ [
+ 'terms' => [
+ 'tags.id' => [
+ $tag->getKey(),
+ ],
+ 'boost' => 1.0,
+ ],
+ ],
+ [
+ 'term' => [
+ 'public' => [
+ 'value' => true,
+ 'boost' => 1.0,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
}
/**
@@ -271,10 +397,191 @@ public function testSearchByTags($user): void
$tag2->getKey(),
],
])
- ->assertOk()
- ->assertJsonCount(2, 'data')
- ->assertJsonFragment(['id' => $product1->getKey()])
- ->assertJsonFragment(['id' => $product2->getKey()]);
+ ->assertOk();
+// ->assertJsonCount(2, 'data')
+// ->assertJsonFragment(['id' => $product1->getKey()])
+// ->assertJsonFragment(['id' => $product2->getKey()]);
+
+ $this->assertElasticQuery([
+ 'bool' => [
+ 'must' => [],
+ 'should' => [],
+ 'filter' => [
+ [
+ 'terms' => [
+ 'tags.id' => [
+ $tag1->getKey(),
+ $tag2->getKey(),
+ ],
+ 'boost' => 1.0,
+ ],
+ ],
+ [
+ 'term' => [
+ 'public' => [
+ 'value' => true,
+ 'boost' => 1.0,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testSearchByMetadata($user): void
+ {
+ $this->$user->givePermissionTo('products.show');
+
+ $this
+ ->actingAs($this->$user)
+ ->json('GET', '/products?metadata.erp_id=1000&metadata.sku=S001')
+ ->assertOk();
+
+ $this->assertElasticQuery([
+ 'bool' => [
+ 'must' => [],
+ 'should' => [],
+ 'filter' => [
+ [
+ 'terms' => [
+ 'metadata.name' => [
+ 'erp_id',
+ 'sku',
+ ],
+ 'boost' => 1.0,
+ ],
+ ],
+ [
+ 'terms' => [
+ 'metadata.value' => [
+ 1000,
+ 'S001',
+ ],
+ 'boost' => 1.0,
+ ],
+ ],
+ [
+ 'term' => [
+ 'public' => [
+ 'value' => true,
+ 'boost' => 1.0,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testSearchByMetadataClassicArray($user): void
+ {
+ $this->$user->givePermissionTo('products.show');
+
+ $this
+ ->actingAs($this->$user)
+ ->json('GET', '/products?metadata[erp_id]=1000&metadata[sku]=S001')
+ ->assertOk();
+
+ $this->assertElasticQuery([
+ 'bool' => [
+ 'must' => [],
+ 'should' => [],
+ 'filter' => [
+ [
+ 'terms' => [
+ 'metadata.name' => [
+ 'erp_id',
+ 'sku',
+ ],
+ 'boost' => 1.0,
+ ],
+ ],
+ [
+ 'terms' => [
+ 'metadata.value' => [
+ 1000,
+ 'S001',
+ ],
+ 'boost' => 1.0,
+ ],
+ ],
+ [
+ 'term' => [
+ 'public' => [
+ 'value' => true,
+ 'boost' => 1.0,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testSearchByMetadataPrivate($user): void
+ {
+ $this->$user->givePermissionTo(['products.show', 'products.show_metadata_private']);
+
+ $this
+ ->actingAs($this->$user)
+ ->json('GET', '/products?metadata_private.sku=S001')
+ ->assertOk();
+
+ $this->assertElasticQuery([
+ 'bool' => [
+ 'must' => [],
+ 'should' => [],
+ 'filter' => [
+ [
+ 'terms' => [
+ 'metadata_private.name' => [
+ 'sku',
+ ],
+ 'boost' => 1.0,
+ ],
+ ],
+ [
+ 'terms' => [
+ 'metadata_private.value' => [
+ 'S001',
+ ],
+ 'boost' => 1.0,
+ ],
+ ],
+ [
+ 'term' => [
+ 'public' => [
+ 'value' => true,
+ 'boost' => 1.0,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testSearchByMetadataPrivateUnauthorized($user): void
+ {
+ $this->$user->givePermissionTo('products.show');
+
+ $this
+ ->actingAs($this->$user)
+ ->json('GET', '/products?metadata_private.sku=S001')
+ ->assertUnprocessable();
}
private function getProductsByParentSet(
@@ -302,7 +609,35 @@ private function getProductsByParentSet(
$childSet->products()->attach($productRef);
- return $this->actingAs($user)
- ->getJson('/products?sets[]=' . $parentSet->slug);
+ $request = $this
+ ->actingAs($user)
+ ->getJson("/products?sets[]={$parentSet->slug}");
+
+ $this->assertElasticQuery([
+ 'bool' => [
+ 'must' => [],
+ 'should' => [],
+ 'filter' => [
+ [
+ 'terms' => [
+ 'sets.slug' => [
+ $parentSet->slug,
+ ],
+ 'boost' => 1.0,
+ ],
+ ],
+ [
+ 'term' => [
+ 'public' => [
+ 'value' => true,
+ 'boost' => 1.0,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
+
+ return $request;
}
}
diff --git a/tests/Feature/ProductSetCreateTest.php b/tests/Feature/ProductSetCreateTest.php
index 1619c61a7..295c791ef 100644
--- a/tests/Feature/ProductSetCreateTest.php
+++ b/tests/Feature/ProductSetCreateTest.php
@@ -5,6 +5,7 @@
use App\Enums\MediaType;
use App\Events\ProductSetCreated;
use App\Listeners\WebHookEventListener;
+use App\Models\Attribute;
use App\Models\Media;
use App\Models\ProductSet;
use App\Models\WebHook;
@@ -640,4 +641,64 @@ public function testCreateWithSeo($user): void
'slug' => 'test',
]);
}
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testCreateWithAttributes($user): void
+ {
+ $this->$user->givePermissionTo('product_sets.add');
+
+ Event::fake([ProductSetCreated::class]);
+
+ $set = [
+ 'name' => 'Test',
+ ];
+
+ $defaults = [
+ 'public' => true,
+ 'hide_on_index' => false,
+ 'slug' => 'test',
+ ];
+
+ $attrOne = Attribute::factory()->create();
+ $attrTwo = Attribute::factory()->create();
+
+ $response = $this->actingAs($this->$user)->postJson('/product-sets', $set + [
+ 'slug_suffix' => 'test',
+ 'slug_override' => false,
+ 'attributes' => [
+ $attrOne->getKey(),
+ $attrTwo->getKey(),
+ ],
+ ]);
+
+ $response
+ ->assertCreated()
+ ->assertJson(['data' => $set + $defaults + [
+ 'slug_override' => false,
+ 'slug_suffix' => 'test',
+ 'parent' => null,
+ ],
+ ]);
+
+ $productSet = ProductSet::find($response->getData()->data->id);
+
+ $this->assertDatabaseHas('product_sets', $set + $defaults + [
+ 'parent_id' => null,
+ ])
+ ->assertDatabaseHas('attribute_product_set', [
+ 'attribute_id' => $attrOne->getKey(),
+ 'product_set_id' => $productSet->getKey(),
+ ])
+ ->assertDatabaseHas('attribute_product_set', [
+ 'attribute_id' => $attrTwo->getKey(),
+ 'product_set_id' => $productSet->getKey(),
+ ]);
+
+ $this->assertTrue($productSet->attributes->contains($attrOne));
+ $this->assertTrue($productSet->attributes->contains($attrTwo));
+
+ Event::assertDispatched(ProductSetCreated::class);
+ }
}
diff --git a/tests/Feature/ProductSetShowTest.php b/tests/Feature/ProductSetShowTest.php
index c19541296..feac8eae3 100644
--- a/tests/Feature/ProductSetShowTest.php
+++ b/tests/Feature/ProductSetShowTest.php
@@ -2,6 +2,7 @@
namespace Tests\Feature;
+use App\Models\Attribute;
use App\Models\ProductSet;
use App\Models\SeoMetadata;
use Tests\TestCase;
@@ -435,4 +436,58 @@ public function testShowSlugTreeHidden($user): void
'data' => $this->expected_structure,
]);
}
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testProductSetHasAttributes($user): void
+ {
+ $this->$user->givePermissionTo(['product_sets.show_details', 'product_sets.show_hidden']);
+
+ $firstAttr = Attribute::factory()->create([
+ 'name' => 'test',
+ 'description' => 'test',
+ 'type' => 'number',
+ 'global' => false,
+ ]);
+ $secondAttr = Attribute::factory()->create([
+ 'name' => 'test2',
+ 'description' => 'test2',
+ 'type' => 'number',
+ 'global' => false,
+ ]);
+
+ $this->set->attributes()->attach([
+ $firstAttr->getKey(),
+ $secondAttr->getKey(),
+ ]);
+
+ $response = $this->actingAs($this->$user)
+ ->getJson('/product-sets/id:' . $this->set->getKey());
+
+ $response
+ ->assertOk()
+ ->assertJson(['data' => [
+ 'id' => $this->set->getKey(),
+ 'name' => $this->set->name,
+ 'slug' => $this->set->slug,
+ 'slug_override' => false,
+ 'public' => $this->set->public,
+ 'visible' => $this->set->public && $this->set->public_parent,
+ 'hide_on_index' => $this->set->hide_on_index,
+ 'parent' => $this->set->parent,
+ 'children_ids' => [
+ $this->childSet->getKey(),
+ ],
+ 'seo' => [
+ 'title' => $this->set->seo->title,
+ 'description' => $this->set->seo->description,
+ ],
+ ],
+ ])
+ ->assertJsonStructure([
+ 'data' => array_merge($this->expected_structure, ['attributes']),
+ ])
+ ->assertJsonCount(2, 'data.attributes');
+ }
}
diff --git a/tests/Feature/ProductSetUpdateTest.php b/tests/Feature/ProductSetUpdateTest.php
index 254e8a120..53b7c2105 100644
--- a/tests/Feature/ProductSetUpdateTest.php
+++ b/tests/Feature/ProductSetUpdateTest.php
@@ -4,6 +4,7 @@
use App\Events\ProductSetUpdated;
use App\Listeners\WebHookEventListener;
+use App\Models\Attribute;
use App\Models\ProductSet;
use App\Models\SeoMetadata;
use App\Models\WebHook;
@@ -396,4 +397,184 @@ public function testUpdateWithSeo($user): void
'description' => 'seo description',
]);
}
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateWithAttributes($user): void
+ {
+ $this->$user->givePermissionTo('product_sets.edit');
+
+ Event::fake([ProductSetUpdated::class]);
+
+ $newSet = ProductSet::factory()->create([
+ 'public' => false,
+ 'order' => 40,
+ ]);
+
+ $attrOne = Attribute::factory()->create();
+ $attrTwo = Attribute::factory()->create();
+ $attrThree = Attribute::factory()->create();
+
+ $newSet->attributes()->sync($attrOne->getKey());
+
+ $set = [
+ 'name' => 'Test Edit',
+ 'public' => true,
+ 'hide_on_index' => true,
+ ];
+
+ $parentId = [
+ 'parent_id' => null,
+ ];
+
+ $response = $this->actingAs($this->$user)->patchJson(
+ '/product-sets/id:' . $newSet->getKey(),
+ $set + $parentId + [
+ 'children_ids' => [],
+ 'slug_suffix' => 'test-edit',
+ 'slug_override' => false,
+ 'attributes' => [
+ $attrTwo->getKey(),
+ $attrThree->getKey(),
+ ],
+ ],
+ );
+
+ $response
+ ->assertOk()
+ ->assertJson(['data' => $set + [
+ 'parent' => null,
+ 'children_ids' => [],
+ 'slug' => 'test-edit',
+ 'slug_suffix' => 'test-edit',
+ 'slug_override' => false,
+ ],
+ ]);
+
+ $this->assertDatabaseHas('product_sets', $set + $parentId + [
+ 'slug' => 'test-edit',
+ ]);
+
+ $this->assertTrue(!$newSet->attributes->contains($attrOne));
+ $this->assertTrue($newSet->attributes->contains($attrTwo));
+ $this->assertTrue($newSet->attributes->contains($attrThree));
+
+ Event::assertDispatched(ProductSetUpdated::class);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateWithEmptyAttributes($user): void
+ {
+ $this->$user->givePermissionTo('product_sets.edit');
+
+ Event::fake([ProductSetUpdated::class]);
+
+ $newSet = ProductSet::factory()->create([
+ 'public' => false,
+ 'order' => 40,
+ ]);
+
+ $attrOne = Attribute::factory()->create();
+
+ $newSet->attributes()->sync($attrOne->getKey());
+
+ $set = [
+ 'name' => 'Test Edit',
+ 'public' => true,
+ 'hide_on_index' => true,
+ ];
+
+ $parentId = [
+ 'parent_id' => null,
+ ];
+
+ $response = $this->actingAs($this->$user)->patchJson(
+ '/product-sets/id:' . $newSet->getKey(),
+ $set + $parentId + [
+ 'children_ids' => [],
+ 'slug_suffix' => 'test-edit',
+ 'slug_override' => false,
+ 'attributes' => [],
+ ],
+ );
+
+ $response
+ ->assertOk()
+ ->assertJson(['data' => $set + [
+ 'parent' => null,
+ 'children_ids' => [],
+ 'slug' => 'test-edit',
+ 'slug_suffix' => 'test-edit',
+ 'slug_override' => false,
+ ],
+ ]);
+
+ $this->assertDatabaseHas('product_sets', $set + $parentId + [
+ 'slug' => 'test-edit',
+ ]);
+
+ $this->assertTrue(!$newSet->attributes->contains($attrOne));
+
+ Event::assertDispatched(ProductSetUpdated::class);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testUpdateWithoutAttributes($user): void
+ {
+ $this->$user->givePermissionTo('product_sets.edit');
+
+ Event::fake([ProductSetUpdated::class]);
+
+ $newSet = ProductSet::factory()->create([
+ 'public' => false,
+ 'order' => 40,
+ ]);
+
+ $attrOne = Attribute::factory()->create();
+
+ $newSet->attributes()->sync($attrOne->getKey());
+
+ $set = [
+ 'name' => 'Test Edit',
+ 'public' => true,
+ 'hide_on_index' => true,
+ ];
+
+ $parentId = [
+ 'parent_id' => null,
+ ];
+
+ $response = $this->actingAs($this->$user)->patchJson(
+ '/product-sets/id:' . $newSet->getKey(),
+ $set + $parentId + [
+ 'children_ids' => [],
+ 'slug_suffix' => 'test-edit',
+ 'slug_override' => false,
+ ],
+ );
+
+ $response
+ ->assertOk()
+ ->assertJson(['data' => $set + [
+ 'parent' => null,
+ 'children_ids' => [],
+ 'slug' => 'test-edit',
+ 'slug_suffix' => 'test-edit',
+ 'slug_override' => false,
+ ],
+ ]);
+
+ $this->assertDatabaseHas('product_sets', $set + $parentId + [
+ 'slug' => 'test-edit',
+ ]);
+
+ $this->assertTrue($newSet->attributes->contains($attrOne));
+
+ Event::assertDispatched(ProductSetUpdated::class);
+ }
}
diff --git a/tests/Feature/ProductTest.php b/tests/Feature/ProductTest.php
index 68b6f2ad9..ddf7196bd 100644
--- a/tests/Feature/ProductTest.php
+++ b/tests/Feature/ProductTest.php
@@ -2,14 +2,19 @@
namespace Tests\Feature;
+use App\Enums\AttributeType;
use App\Enums\MediaType;
+use App\Enums\MetadataType;
use App\Enums\SchemaType;
use App\Events\ProductCreated;
use App\Events\ProductDeleted;
use App\Events\ProductUpdated;
use App\Listeners\WebHookEventListener;
+use App\Models\Attribute;
+use App\Models\AttributeOption;
use App\Models\Media;
use App\Models\Product;
+use App\Models\ProductAttribute;
use App\Models\ProductSet;
use App\Models\Schema;
use App\Models\SeoMetadata;
@@ -25,15 +30,20 @@
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Spatie\WebhookServer\CallWebhookJob;
+use Tests\Support\ElasticTest;
use Tests\TestCase;
class ProductTest extends TestCase
{
+ use ElasticTest;
+
private Product $product;
private Product $hidden_product;
private array $expected;
private array $expected_short;
+ private array $expected_attribute;
+ private array $expected_attribute_short;
private ProductServiceContract $productService;
private AvailabilityServiceContract $availabilityService;
@@ -85,10 +95,28 @@ public function setUp(): void
'quantity' => 10,
]);
+ $metadata = $this->product->metadata()->create([
+ 'name' => 'testMetadata',
+ 'value' => 'value metadata',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
$this->hidden_product = Product::factory()->create([
'public' => false,
]);
+ $attribute = Attribute::factory()->create();
+
+ $option = AttributeOption::factory()->create([
+ 'index' => 1,
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $this->product->attributes()->attach($attribute->getKey());
+
+ $this->product->attributes->first()->pivot->options()->attach($option->getKey());
+
$this->availabilityService->calculateAvailabilityOnOrderAndRestock($item);
/**
@@ -105,10 +133,38 @@ public function setUp(): void
'cover' => null,
];
+ $this->expected_attribute_short = [
+ 'attributes' => [
+ [
+ 'name' => $attribute->name,
+ 'selected_options' => [
+ [
+ 'id' => $option->getKey(),
+ 'name' => $option->name,
+ 'index' => $option->index,
+ 'value_number' => $option->value_number,
+ 'value_date' => $option->value_date,
+ 'attribute_id' => $attribute->getKey(),
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ $this->expected_attribute = $this->expected_attribute_short;
+ $this->expected_attribute['attributes'][0] += [
+ 'id' => $attribute->getKey(),
+ 'slug' => $attribute->slug,
+ 'description' => $attribute->description,
+ 'type' => $attribute->type,
+ 'global' => $attribute->global,
+ 'sortable' => $attribute->sortable,
+ ];
+
/**
* Expected full response
*/
- $this->expected = array_merge($this->expected_short, [
+ $this->expected = array_merge($this->expected_short, $this->expected_attribute, [
'description_html' => $this->product->description_html,
'description_short' => $this->product->description_short,
'meta_description' => strip_tags($this->product->description_html),
@@ -119,6 +175,7 @@ public function setUp(): void
'required' => true,
'available' => true,
'price' => 0,
+ 'metadata' => [],
'options' => [
[
'name' => 'XL',
@@ -130,6 +187,7 @@ public function setUp(): void
'sku' => 'K001/XL',
],
],
+ 'metadata' => [],
],
[
'name' => 'L',
@@ -141,10 +199,14 @@ public function setUp(): void
'sku' => 'K001/L',
],
],
+ 'metadata' => [],
],
],
],
],
+ 'metadata' => [
+ $metadata->name => $metadata->value,
+ ],
]);
}
@@ -169,14 +231,39 @@ public function testIndex($user): void
]);
$product->sets()->sync([$set->getKey()]);
- $response = $this->actingAs($this->$user)->getJson('/products?limit=100');
- $response
- ->assertOk()
- ->assertJsonCount(1, 'data') // Should show only public products.
- ->assertJson(['data' => [
- 0 => $this->expected_short,
+ $this
+ ->actingAs($this->$user)
+ ->json('GET', '/products', ['limit' => 100])
+ ->assertOk();
+// ->assertJsonCount(1, 'data') // Should show only public products.
+// ->assertJson(['data' => [
+// 0 => $this->expected_short,
+// ]]);
+
+ $this->assertElasticQuery([
+ 'bool' => [
+ 'must' => [],
+ 'should' => [],
+ 'filter' => [
+ [
+ 'term' => [
+ 'public' => [
+ 'value' => true,
+ 'boost' => 1.0,
+ ],
+ ],
+ ],
+ [
+ 'term' => [
+ 'hide_on_index' => [
+ 'value' => false,
+ 'boost' => 1.0,
+ ],
+ ],
+ ],
+ ],
],
- ]);
+ ], 100);
$this->assertQueryCountLessThan(20);
}
@@ -203,12 +290,39 @@ public function testIndexIdsSearch($user): void
'created_at' => Carbon::now()->addHour(),
]);
- $response = $this->actingAs($this->$user)
- ->json('GET', "/products?ids={$firstProduct->getKey()},{$secondProduct->getKey()}");
-
- $response
- ->assertOk()
- ->assertJsonCount(2, 'data');
+ $this
+ ->actingAs($this->$user)
+ ->json('GET', '/products', [
+ 'ids' => "{$firstProduct->getKey()},{$secondProduct->getKey()}",
+ ])
+ ->assertOk();
+// ->assertJsonCount(2, 'data');
+
+ $this->assertElasticQuery([
+ 'bool' => [
+ 'must' => [],
+ 'should' => [],
+ 'filter' => [
+ [
+ 'terms' => [
+ 'id' => [
+ $firstProduct->getKey(),
+ $secondProduct->getKey(),
+ ],
+ 'boost' => 1.0,
+ ],
+ ],
+ [
+ 'term' => [
+ 'public' => [
+ 'value' => true,
+ 'boost' => 1.0,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
}
/**
@@ -228,52 +342,18 @@ public function testIndexHidden($user): void
$product->sets()->sync([$set->getKey()]);
- $response = $this->actingAs($this->$user)->getJson('/products');
- $response
- ->assertOk()
- ->assertJsonCount(3, 'data'); // Should show all products.
- }
-
- /**
- * @dataProvider authProvider
- */
- public function testIndexPerformance($user): void
- {
- $this->$user->givePermissionTo('products.show');
+ $this->actingAs($this->$user)
+ ->json('GET', '/products')
+ ->assertOk();
+// ->assertJsonCount(3, 'data'); // Should show all products.
- Product::factory()->count(499)->create([
- 'public' => true,
- 'order' => 1,
- ]);
-
- $this
- ->actingAs($this->$user)
- ->getJson('/products?limit=500')
- ->assertOk()
- ->assertJsonCount(500, 'data');
-
- $this->assertQueryCountLessThan(20);
- }
-
- /**
- * @dataProvider authProvider
- */
- public function testIndexFullPerformance($user): void
- {
- $this->$user->givePermissionTo('products.show');
-
- Product::factory()->count(499)->create([
- 'public' => true,
- 'order' => 1,
+ $this->assertElasticQuery([
+ 'bool' => [
+ 'must' => [],
+ 'should' => [],
+ 'filter' => [],
+ ],
]);
-
- $this
- ->actingAs($this->$user)
- ->getJson('/products?limit=500&full')
- ->assertOk()
- ->assertJsonCount(500, 'data');
-
- $this->assertQueryCountLessThan(20);
}
public function testShowUnauthorized(): void
@@ -342,6 +422,7 @@ public function testShowSets($user): void
'parent_id' => $set1->parent_id,
'children_ids' => [],
'cover' => null,
+ 'metadata' => [],
],
[
'id' => $set2->getKey(),
@@ -355,6 +436,7 @@ public function testShowSets($user): void
'parent_id' => $set2->parent_id,
'children_ids' => [],
'cover' => null,
+ 'metadata' => [],
],
],
]);
@@ -408,12 +490,14 @@ public function testShowSetsWithCover($user): void
'hide_on_index' => $set1->hide_on_index,
'parent_id' => $set1->parent_id,
'children_ids' => [],
+ 'metadata' => [],
'cover' => [
'id' => $media1->getKey(),
'type' => Str::lower($media1->type->key),
'url' => $media1->url,
'slug' => $media1->slug,
'alt' => $media1->alt,
+ 'metadata' => [],
],
],
[
@@ -427,18 +511,88 @@ public function testShowSetsWithCover($user): void
'hide_on_index' => $set2->hide_on_index,
'parent_id' => $set2->parent_id,
'children_ids' => [],
+ 'metadata' => [],
'cover' => [
'id' => $media2->getKey(),
'type' => Str::lower($media2->type->key),
'url' => $media2->url,
'slug' => $media2->slug,
'alt' => $media2->alt,
+ 'metadata' => [],
],
],
],
]);
}
+ /**
+ * @dataProvider authProvider
+ */
+ public function testShowAttributes($user): void
+ {
+ $this->$user->givePermissionTo('products.show_details');
+
+ $product = Product::factory()->create([
+ 'public' => true,
+ ]);
+
+ $attribute = Attribute::factory()->create();
+
+ $option = AttributeOption::factory()->create([
+ 'index' => 1,
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $product->attributes()->attach($attribute->getKey());
+
+ $product->attributes->first()->pivot->options()->attach($option->getKey());
+
+ $this
+ ->actingAs($this->$user)
+ ->getJson('/products/' . $product->slug)
+ ->assertOk()
+ ->assertJsonFragment([
+ 'id' => $attribute->getKey(),
+ 'name' => $attribute->name,
+ 'description' => $attribute->description,
+ 'type' => $attribute->type,
+ 'global' => $attribute->global,
+ ])
+ ->assertJsonFragment([
+ 'id' => $option->getKey(),
+ 'name' => $option->name,
+ 'index' => $option->index,
+ 'value_number' => $option->value_number,
+ 'value_date' => $option->value_date,
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testShowPrivateMetadata($user): void
+ {
+ $this->$user->givePermissionTo(['products.show_details', 'products.show_metadata_private']);
+
+ $privateMetadata = $this->product->metadataPrivate()->create([
+ 'name' => 'hiddenMetadata',
+ 'value' => 'hidden metadata test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => false,
+ ]);
+
+ $response = $this->actingAs($this->$user)
+ ->getJson('/products/id:' . $this->product->getKey());
+
+ $response
+ ->assertOk()
+ ->assertJsonFragment(['metadata_private' => [
+ $privateMetadata->name => $privateMetadata->value,
+ ],
+ ]);
+ }
+
/**
* @dataProvider authProvider
*/
@@ -1128,7 +1282,7 @@ public function testCreateMinMaxPrice($user): void
$schemaPrice = 50;
$schema = Schema::factory()->create([
- 'type' => 0,
+ 'type' => SchemaType::STRING,
'required' => false,
'price' => $schemaPrice,
]);
@@ -1158,6 +1312,312 @@ public function testCreateMinMaxPrice($user): void
]);
}
+ /**
+ * @dataProvider authProvider
+ */
+ public function testCreateMinPriceWithRequiredSchema($user): void
+ {
+ $this->$user->givePermissionTo('products.add');
+
+ $schemaPrice = 50;
+ $schema = Schema::factory()->create([
+ 'type' => SchemaType::STRING,
+ 'required' => true,
+ 'price' => $schemaPrice,
+ ]);
+
+ $productPrice = 150;
+ $response = $this->actingAs($this->$user)->postJson('/products', [
+ 'name' => 'Test',
+ 'slug' => 'test',
+ 'price' => $productPrice,
+ 'public' => false,
+ 'sets' => [],
+ 'schemas' => [
+ $schema->getKey(),
+ ],
+ ]);
+
+ $response->assertCreated();
+
+ $this->assertDatabaseHas('products', [
+ 'slug' => 'test',
+ 'name' => 'Test',
+ 'price' => $productPrice,
+ 'price_min' => $productPrice + $schemaPrice,
+ 'price_max' => $productPrice + $schemaPrice,
+ 'public' => false,
+ 'description_html' => null,
+ ]);
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testCreateWithAttribute($user): void
+ {
+ $this->$user->givePermissionTo('products.add');
+
+ $attribute = Attribute::factory()->create();
+
+ $option = AttributeOption::factory()->create([
+ 'index' => 1,
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $attribute2 = Attribute::factory()->create();
+
+ $option2 = AttributeOption::factory()->create([
+ 'index' => 2,
+ 'attribute_id' => $attribute2->getKey(),
+ ]);
+
+ $response = $this
+ ->actingAs($this->$user)
+ ->postJson('/products', [
+ 'name' => 'Test',
+ 'slug' => 'test',
+ 'price' => 0,
+ 'public' => true,
+ 'attributes' => [
+ $attribute->getKey() => [
+ $option->getKey(),
+ ],
+ $attribute2->getKey() => [
+ $option2->getKey(),
+ ],
+ ],
+ ])
+ ->assertCreated()
+ ->assertJsonFragment([
+ 'id' => $attribute->getKey(),
+ 'name' => $attribute->name,
+ 'slug' => $attribute->slug,
+ 'description' => $attribute->description,
+ 'type' => $attribute->type,
+ 'global' => $attribute->global,
+ 'sortable' => $attribute->sortable,
+ ])
+ ->assertJsonFragment([
+ 'id' => $option->getKey(),
+ 'name' => $option->name,
+ 'index' => $option->index,
+ 'value_number' => $option->value_number,
+ 'value_date' => $option->value_date,
+ 'attribute_id' => $attribute->getKey(),
+ ])
+ ->assertJsonFragment([
+ 'id' => $attribute2->getKey(),
+ 'name' => $attribute2->name,
+ 'slug' => $attribute2->slug,
+ 'description' => $attribute2->description,
+ 'type' => $attribute2->type,
+ 'global' => $attribute2->global,
+ 'sortable' => $attribute2->sortable,
+ ])
+ ->assertJsonFragment([
+ 'id' => $option2->getKey(),
+ 'name' => $option2->name,
+ 'index' => $option2->index,
+ 'value_number' => $option2->value_number,
+ 'value_date' => $option2->value_date,
+ 'attribute_id' => $attribute2->getKey(),
+ ]);
+
+ $this->assertDatabaseHas('products', [
+ 'slug' => 'test',
+ 'name' => 'Test',
+ 'price' => 0,
+ ]);
+
+ $product = Product::find($response->getData()->data->id);
+
+ $productAttribute1 = ProductAttribute::where('product_id', $product->getKey())
+ ->where('attribute_id', $attribute->getKey())
+ ->first();
+ $productAttribute2 = ProductAttribute::where('product_id', $product->getKey())
+ ->where('attribute_id', $attribute2->getKey())
+ ->first();
+
+ $this->assertDatabaseHas('product_attribute', [
+ 'product_id' => $product->getKey(),
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+ $this->assertDatabaseHas('product_attribute', [
+ 'product_id' => $product->getKey(),
+ 'attribute_id' => $attribute2->getKey(),
+ ]);
+
+ $this->assertDatabaseHas('product_attribute_attribute_option', [
+ 'product_attribute_id' => $productAttribute1->getKey(),
+ 'attribute_option_id' => $option->getKey(),
+ ]);
+
+ $this->assertDatabaseHas('product_attribute_attribute_option', [
+ 'product_attribute_id' => $productAttribute2->getKey(),
+ 'attribute_option_id' => $option2->getKey(),
+ ]);
+
+ $this->assertDatabaseCount('product_attribute_attribute_option', 3); // +1 from $this->product
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testCreateWithAttributeMultipleOptions($user): void
+ {
+ $this->$user->givePermissionTo('products.add');
+
+ $attribute = Attribute::factory()->create([
+ 'type' => AttributeType::MULTI_CHOICE_OPTION,
+ ]);
+
+ $option = $attribute->options()->create([
+ 'index' => 1,
+ ]);
+
+ $option2 = $attribute->options()->create([
+ 'index' => 1,
+ ]);
+
+ $response = $this
+ ->actingAs($this->$user)
+ ->postJson('/products', [
+ 'name' => 'Test',
+ 'slug' => 'test',
+ 'price' => 0,
+ 'public' => true,
+ 'attributes' => [
+ $attribute->getKey() => [
+ $option->getKey(),
+ $option2->getKey(),
+ ],
+ ],
+ ])
+ ->assertCreated()
+ ->assertJsonFragment([
+ 'id' => $attribute->getKey(),
+ 'name' => $attribute->name,
+ 'slug' => $attribute->slug,
+ 'description' => $attribute->description,
+ 'type' => $attribute->type,
+ 'global' => $attribute->global,
+ 'sortable' => $attribute->sortable,
+ ])
+ ->assertJsonFragment([
+ 'id' => $option->getKey(),
+ 'name' => $option->name,
+ 'index' => $option->index,
+ 'value_number' => $option->value_number,
+ 'value_date' => $option->value_date,
+ 'attribute_id' => $attribute->getKey(),
+ ])
+ ->assertJsonFragment([
+ 'id' => $option2->getKey(),
+ 'name' => $option2->name,
+ 'index' => $option2->index,
+ 'value_number' => $option2->value_number,
+ 'value_date' => $option2->value_date,
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $this->assertDatabaseHas('products', [
+ 'slug' => 'test',
+ 'name' => 'Test',
+ 'price' => 0,
+ ]);
+
+ $product = Product::find($response->getData()->data->id);
+
+ $productAttribute = ProductAttribute::where('product_id', $product->getKey())
+ ->where('attribute_id', $attribute->getKey())
+ ->first();
+
+ $this->assertDatabaseHas('product_attribute', [
+ 'product_id' => $product->getKey(),
+ 'attribute_id' => $attribute->getKey(),
+ ]);
+
+ $this->assertDatabaseHas('product_attribute_attribute_option', [
+ 'product_attribute_id' => $productAttribute->getKey(),
+ 'attribute_option_id' => $option->getKey(),
+ ]);
+
+ $this->assertDatabaseHas('product_attribute_attribute_option', [
+ 'product_attribute_id' => $productAttribute->getKey(),
+ 'attribute_option_id' => $option2->getKey(),
+ ]);
+
+ $this->assertDatabaseCount('product_attribute_attribute_option', 3); // +1 from $this->product
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testCreateWithAttributeInvalidMultipleOptions($user): void
+ {
+ $this->$user->givePermissionTo('products.add');
+
+ $attribute = Attribute::factory()->create([
+ 'type' => AttributeType::SINGLE_OPTION,
+ ]);
+
+ $option = $attribute->options()->create([
+ 'index' => 1,
+ ]);
+
+ $option2 = $attribute->options()->create([
+ 'index' => 1,
+ ]);
+
+ $this
+ ->actingAs($this->$user)
+ ->postJson('/products', [
+ 'name' => 'Test',
+ 'slug' => 'test',
+ 'price' => 0,
+ 'public' => true,
+ 'attributes' => [
+ $attribute->getKey() => [
+ $option->getKey(),
+ $option2->getKey(),
+ ],
+ ],
+ ])
+ ->assertUnprocessable();
+ }
+
+ /**
+ * @dataProvider authProvider
+ */
+ public function testCreateWithAttributeInvalidOption($user): void
+ {
+ $this->$user->givePermissionTo('products.add');
+
+ $attribute = Attribute::factory()->create();
+
+ $attribute2 = Attribute::factory()->create();
+
+ $option = $attribute2->options()->create([
+ 'index' => 1,
+ ]);
+
+ $this
+ ->actingAs($this->$user)
+ ->postJson('/products', [
+ 'name' => 'Test',
+ 'slug' => 'test',
+ 'price' => 0,
+ 'public' => true,
+ 'attributes' => [
+ $attribute->getKey() => [
+ $option->getKey(),
+ ],
+ ],
+ ])
+ ->assertUnprocessable();
+ }
+
public function testUpdateUnauthorized(): void
{
Event::fake([ProductUpdated::class]);
diff --git a/tests/Feature/RoleTest.php b/tests/Feature/RoleTest.php
index 066d57e7e..e40907f59 100644
--- a/tests/Feature/RoleTest.php
+++ b/tests/Feature/RoleTest.php
@@ -50,6 +50,7 @@ public function testIndex($user): void
'description' => $role1->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
])
->assertJsonFragment([[
@@ -58,6 +59,7 @@ public function testIndex($user): void
'description' => $role2->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
]);
}
@@ -99,6 +101,7 @@ public function testIndexSearchByName($user): void
'description' => $role1->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
])
->assertJsonFragment([[
@@ -107,6 +110,7 @@ public function testIndexSearchByName($user): void
'description' => $role2->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
]);
}
@@ -148,6 +152,7 @@ public function testIndexSearchByDescription($user): void
'description' => $role1->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
])
->assertJsonFragment([[
@@ -156,6 +161,7 @@ public function testIndexSearchByDescription($user): void
'description' => $role2->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
]);
}
@@ -220,6 +226,7 @@ public function testIndexSearchByAssignable($user): void
'description' => $roleNoPermissions->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
])
->assertJsonFragment([[
@@ -228,6 +235,7 @@ public function testIndexSearchByAssignable($user): void
'description' => $roleHasPermissions->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
]);
}
@@ -292,6 +300,7 @@ public function testIndexSearchByUnassignable($user): void
'description' => $roleHasSomePermissions->description,
'assignable' => false,
'deletable' => true,
+ 'metadata' => [],
],
])
->assertJsonFragment([[
@@ -300,6 +309,7 @@ public function testIndexSearchByUnassignable($user): void
'description' => $roleHasNoPermissions->description,
'assignable' => false,
'deletable' => true,
+ 'metadata' => [],
],
])
->assertJsonFragment([[
@@ -308,6 +318,7 @@ public function testIndexSearchByUnassignable($user): void
'description' => $roleUnauthenticated->description,
'assignable' => false,
'deletable' => false,
+ 'metadata' => [],
],
])
->assertJsonFragment([[
@@ -316,6 +327,7 @@ public function testIndexSearchByUnassignable($user): void
'description' => $roleAuthenticated->description,
'assignable' => false,
'deletable' => false,
+ 'metadata' => [],
],
]);
}
@@ -357,6 +369,7 @@ public function testIndexSearch($user): void
'description' => $role1->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
])
->assertJsonFragment([[
@@ -365,6 +378,7 @@ public function testIndexSearch($user): void
'description' => $role2->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
]);
}
@@ -403,6 +417,7 @@ public function testShow($user): void
'assignable' => true,
'deletable' => true,
'permissions' => [],
+ 'metadata' => [],
],
]);
}
@@ -437,6 +452,7 @@ public function testShowPermissions($user): void
'test.custom1',
'test.custom2',
],
+ 'metadata' => [],
],
]);
}
@@ -471,6 +487,7 @@ public function testShowPermissionsAssignable($user): void
'test.custom1',
'test.custom2',
],
+ 'metadata' => [],
],
]);
}
@@ -508,6 +525,7 @@ public function testShowUnauthenticatedRoleUnassignable($user): void
'test.custom1',
'test.custom2',
],
+ 'metadata' => [],
],
]);
}
@@ -586,6 +604,7 @@ public function testCreate($user): void
'test.custom1',
'test.custom2',
],
+ 'metadata' => [],
],
]);
@@ -617,6 +636,7 @@ public function testCreateWithoutDescription($user): void
'assignable' => true,
'deletable' => true,
'permissions' => [],
+ 'metadata' => [],
],
]);
@@ -787,6 +807,7 @@ public function testUpdate($user): void
'test.custom2',
'test.custom3',
],
+ 'metadata' => [],
],
]);
@@ -826,6 +847,7 @@ public function testUpdateNameOnly($user): void
'assignable' => true,
'deletable' => true,
'permissions' => [],
+ 'metadata' => [],
],
]);
@@ -859,6 +881,7 @@ public function testUpdateDescriptionOnly($user): void
'assignable' => true,
'deletable' => true,
'permissions' => [],
+ 'metadata' => [],
],
]);
@@ -892,6 +915,7 @@ public function testUpdateDescriptionRemove($user): void
'assignable' => true,
'deletable' => true,
'permissions' => [],
+ 'metadata' => [],
],
]);
diff --git a/tests/Feature/ShippingMethodTest.php b/tests/Feature/ShippingMethodTest.php
index f47f9754a..e6422c3c0 100644
--- a/tests/Feature/ShippingMethodTest.php
+++ b/tests/Feature/ShippingMethodTest.php
@@ -64,6 +64,7 @@ public function setUp(): void
'public' => $this->shipping_method->public,
'shipping_time_min' => $this->shipping_method->shipping_time_min,
'shipping_time_max' => $this->shipping_method->shipping_time_max,
+ 'metadata' => [],
];
$this->priceRangesWithNoInitialStart = [
diff --git a/tests/Feature/StatusTest.php b/tests/Feature/StatusTest.php
index a4bfae274..c32456b1b 100644
--- a/tests/Feature/StatusTest.php
+++ b/tests/Feature/StatusTest.php
@@ -31,6 +31,7 @@ public function setUp(): void
'description' => $this->status_model->description,
'hidden' => $this->status_model->hidden,
'no_notifications' => $this->status_model->no_notifications,
+ 'metadata' => [],
];
}
diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php
index 0d0c520ff..c00de850c 100644
--- a/tests/Feature/UserTest.php
+++ b/tests/Feature/UserTest.php
@@ -2,6 +2,7 @@
namespace Tests\Feature;
+use App\Enums\MetadataType;
use App\Enums\RoleType;
use App\Events\UserCreated;
use App\Events\UserDeleted;
@@ -34,6 +35,13 @@ public function setUp(): void
{
parent::setUp();
+ $metadata = $this->user->metadata()->create([
+ 'name' => 'Metadata',
+ 'value' => 'metadata test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => true,
+ ]);
+
$this->expected = [
'id' => $this->user->getKey(),
'email' => $this->user->email,
@@ -41,6 +49,9 @@ public function setUp(): void
'avatar' => $this->user->avatar,
'roles' => [],
'is_tfa_active' => $this->user->is_tfa_active,
+ 'metadata' => [
+ $metadata->name => $metadata->value,
+ ],
];
// Owner role needs to exist for user service to function properly
@@ -203,6 +214,7 @@ public function testIndexNameSearch($user): void
'avatar' => $otherUser->avatar,
'roles' => [],
'is_tfa_active' => $otherUser->is_tfa_active,
+ 'metadata' => [],
]);
}
@@ -229,6 +241,7 @@ public function testIndexEmailSearch($user): void
'avatar' => $otherUser->avatar,
'roles' => [],
'is_tfa_active' => $otherUser->is_tfa_active,
+ 'metadata' => [],
]);
}
@@ -254,6 +267,7 @@ public function testIndexFullSearchName($user): void
'avatar' => $otherUser->avatar,
'roles' => [],
'is_tfa_active' => $otherUser->is_tfa_active,
+ 'metadata' => [],
]);
}
@@ -279,6 +293,7 @@ public function testIndexFullSearchEmail($user): void
'avatar' => $otherUser->avatar,
'roles' => [],
'is_tfa_active' => $otherUser->is_tfa_active,
+ 'metadata' => [],
]);
}
@@ -301,6 +316,31 @@ public function testShow($user): void
->assertJson(['data' => $this->expected + ['permissions' => []]]);
}
+ /**
+ * @dataProvider authProvider
+ */
+ public function testShowPrivateMetadata($user): void
+ {
+ $this->$user->givePermissionTo(['users.show_details', 'users.show_metadata_private']);
+
+ $privateMetadata = $this->user->metadataPrivate()->create([
+ 'name' => 'hiddenMetadata',
+ 'value' => 'hidden metadata test',
+ 'value_type' => MetadataType::STRING,
+ 'public' => false,
+ ]);
+
+ $response = $this->actingAs($this->$user)->getJson('/users/id:' . $this->user->getKey());
+ $response
+ ->assertOk()
+ ->assertJson(['data' => $this->expected +
+ [
+ 'permissions' => [],
+ 'metadata_private' => [$privateMetadata->name => $privateMetadata->value],
+ ],
+ ]);
+ }
+
public function testCreateUnauthorized(): void
{
Event::fake([UserCreated::class]);
@@ -552,6 +592,7 @@ public function testCreateRoles($user): void
'description' => $role1->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
])->assertJsonFragment([[
'id' => $role2->getKey(),
@@ -559,6 +600,7 @@ public function testCreateRoles($user): void
'description' => $role2->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
])->assertJsonFragment([[
'id' => $role3->getKey(),
@@ -566,6 +608,7 @@ public function testCreateRoles($user): void
'description' => $role3->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
])->assertJsonFragment([[
'id' => $this->authenticated->getKey(),
@@ -573,6 +616,7 @@ public function testCreateRoles($user): void
'description' => $this->authenticated->description,
'assignable' => false,
'deletable' => false,
+ 'metadata' => [],
],
])->assertJsonPath('data.permissions', $permissions);
@@ -820,6 +864,7 @@ public function testUpdateAddRoles($user): void
'description' => $role1->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
])->assertJsonFragment([[
'id' => $role2->getKey(),
@@ -827,6 +872,7 @@ public function testUpdateAddRoles($user): void
'description' => $role2->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
])->assertJsonFragment([[
'id' => $role3->getKey(),
@@ -834,6 +880,7 @@ public function testUpdateAddRoles($user): void
'description' => $role3->description,
'assignable' => true,
'deletable' => true,
+ 'metadata' => [],
],
])->assertJsonFragment([[
'id' => $this->authenticated->getKey(),
@@ -841,6 +888,7 @@ public function testUpdateAddRoles($user): void
'description' => $this->authenticated->description,
'assignable' => false,
'deletable' => false,
+ 'metadata' => [],
],
])->assertJsonPath('data.permissions', $permissions);
@@ -959,6 +1007,7 @@ public function testUpdateRemoveRoles($user): void
'description' => $this->authenticated->description,
'assignable' => false,
'deletable' => false,
+ 'metadata' => [],
],
])
->assertJsonPath('data.permissions', $this->authenticatedPermissions->toArray());
diff --git a/tests/Support/ElasticTest.php b/tests/Support/ElasticTest.php
new file mode 100644
index 000000000..45954f855
--- /dev/null
+++ b/tests/Support/ElasticTest.php
@@ -0,0 +1,34 @@
+instance(
+ ElasticClientFactory::class,
+ ElasticClientFactory::fake(new FakeResponse(200, $response)),
+ );
+ }
+
+ public function assertElasticQuery(array $query, ?int $limit = null): void
+ {
+ $this->assertEquals(
+ [
+ 'query' => $query,
+ 'from' => 0,
+ 'size' => $limit ?? Config::get('pagination.per_page'),
+ ],
+ ElasticEngine::debug()->array(),
+ 'Failed to assert that elastic query matched the expected',
+ );
+ }
+}
diff --git a/tests/Support/fake-response.json b/tests/Support/fake-response.json
new file mode 100644
index 000000000..383859cba
--- /dev/null
+++ b/tests/Support/fake-response.json
@@ -0,0 +1,18 @@
+{
+ "took" : 4,
+ "timed_out" : false,
+ "_shards" : {
+ "total" : 1,
+ "successful" : 1,
+ "skipped" : 0,
+ "failed" : 0
+ },
+ "hits" : {
+ "total" : {
+ "value" : 0,
+ "relation" : "eq"
+ },
+ "max_score" : 3.4428797,
+ "hits" : []
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index b87967621..ad5298600 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -14,11 +14,12 @@
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
+use Tests\Support\ElasticTest;
use Tests\Traits\JsonQueryCounter;
abstract class TestCase extends BaseTestCase
{
- use CreatesApplication, RefreshDatabase, JsonQueryCounter;
+ use CreatesApplication, RefreshDatabase, JsonQueryCounter, ElasticTest;
public User $user;
public Application $application;
@@ -29,7 +30,9 @@ abstract class TestCase extends BaseTestCase
public function setUp(): void
{
parent::setUp();
- ini_set('memory_limit', '1024M');
+ ini_set('memory_limit', '4096M');
+
+ $this->fakeElastic();
$this->tokenService = App::make(TokenServiceContract::class);
@@ -44,6 +47,13 @@ public function setUp(): void
$this->application = Application::factory()->create();
}
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+
+ app()->forgetInstances();
+ }
+
public function authProvider(): array
{
return [
diff --git a/tests/Unit/ProductServiceTest.php b/tests/Unit/ProductServiceTest.php
index 3b0bc082d..d7656b851 100644
--- a/tests/Unit/ProductServiceTest.php
+++ b/tests/Unit/ProductServiceTest.php
@@ -6,14 +6,11 @@
use App\Models\Product;
use App\Models\Schema;
use App\Services\Contracts\ProductServiceContract;
-use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\App;
use Tests\TestCase;
class ProductServiceTest extends TestCase
{
- use RefreshDatabase;
-
private ProductServiceContract $productService;
public function setUp(): void
@@ -26,6 +23,7 @@ public function setUp(): void
public function testMinMaxPricesNoSchemas(): void
{
$price = 10;
+ /** @var Product $product */
$product = Product::factory()->create([
'price' => $price,
]);