diff --git a/app/GraphQL/Ecosystem/Mutations/Filesystem/FilesystemMapperMutation.php b/app/GraphQL/Ecosystem/Mutations/Filesystem/FilesystemMapperMutation.php index 3a12817b90..db5be0d574 100644 --- a/app/GraphQL/Ecosystem/Mutations/Filesystem/FilesystemMapperMutation.php +++ b/app/GraphQL/Ecosystem/Mutations/Filesystem/FilesystemMapperMutation.php @@ -22,6 +22,7 @@ use Kanvas\Regions\Models\Regions; use Kanvas\SystemModules\Models\SystemModules; use Kanvas\SystemModules\Services\SystemModulesServices; +use Kanvas\Users\Repositories\UsersRepository; use League\Csv\Reader; class FilesystemMapperMutation @@ -117,6 +118,12 @@ public function process(mixed $root, array $req): FilesystemImports ? Regions::getByIdFromCompanyAppOrGlobal($input['regions_id'], $company, $app) : Regions::getDefault($company, $app); + $extra = is_array($input['extra'] ?? null) ? $input['extra'] : []; + if (! empty($input['users_id'])) { + $owner = UsersRepository::getUserOfCompanyById($company, (int) $input['users_id']); + $extra['users_id'] = $owner->getId(); + } + $dto = FilesystemImport::from([ 'app' => $app, 'users' => $user, @@ -125,7 +132,7 @@ public function process(mixed $root, array $req): FilesystemImports 'companiesBranches' => $user->getCurrentBranch(), 'filesystem' => $filesystem, 'filesystemMapper' => $mapper, - 'extra' => $input['extra'] ?? null, + 'extra' => $extra, ]); if ($app->get(AppSettingsEnums::FILESYSTEM_MAPPER_HEADER_VALIDATION->getValue())) { diff --git a/graphql/schemas/Ecosystem/Filesystem/mapper.graphql b/graphql/schemas/Ecosystem/Filesystem/mapper.graphql index 8bd49e8386..d1059dfd99 100644 --- a/graphql/schemas/Ecosystem/Filesystem/mapper.graphql +++ b/graphql/schemas/Ecosystem/Filesystem/mapper.graphql @@ -41,6 +41,7 @@ input FilesystemImportInput { regions_id: ID filesystem_mapper_id: ID! filesystem_id: ID! + users_id: ID extra: Mixed } diff --git a/src/Domains/Inventory/Importer/Actions/ProductImporterAction.php b/src/Domains/Inventory/Importer/Actions/ProductImporterAction.php index 9fb3c27ffa..3431770bf1 100644 --- a/src/Domains/Inventory/Importer/Actions/ProductImporterAction.php +++ b/src/Domains/Inventory/Importer/Actions/ProductImporterAction.php @@ -45,7 +45,8 @@ public function __construct( public UserInterface $user, public KanvasRegions|Regions $region, public ?AppInterface $app = null, - public bool $runWorkflow = true + public bool $runWorkflow = true, + public ?UserInterface $ownerUser = null ) { $this->app = $this->app ?? app(Apps::class); } @@ -59,6 +60,7 @@ public function execute(): ProductsModel 'app' => $this->app, 'company' => $this->company, 'user' => $this->user, + 'ownerUser' => $this->ownerUser, 'name' => $this->importedProduct->name, 'slug' => $this->importedProduct->slug, 'description' => $this->importedProduct->description, diff --git a/src/Domains/Inventory/Importer/Jobs/ProductImporterJob.php b/src/Domains/Inventory/Importer/Jobs/ProductImporterJob.php index cf29c0c896..21afa64468 100644 --- a/src/Domains/Inventory/Importer/Jobs/ProductImporterJob.php +++ b/src/Domains/Inventory/Importer/Jobs/ProductImporterJob.php @@ -11,6 +11,7 @@ use Kanvas\Inventory\Importer\Actions\ProductImporterAction; use Kanvas\Inventory\Importer\DataTransferObjects\ProductImporter; use Kanvas\Inventory\Importer\Events\ProductImportEvent; +use Kanvas\Users\Repositories\UsersRepository; use Kanvas\Workflow\Enums\WorkflowEnum; use Nuwave\Lighthouse\Execution\Utils\Subscription; use Override; @@ -44,6 +45,14 @@ public function handle() $this->startFilesystemMapperImport(); $processProducts = []; + // Optional owner: stamps products/variants users_id; $this->user still gates auth + publishing. + $ownerUser = null; + $extra = $this->filesystemImport?->extra; + $ownerId = is_array($extra) ? ($extra['users_id'] ?? null) : null; + if (! empty($ownerId)) { + $ownerUser = UsersRepository::getUserOfCompanyById($company, (int) $ownerId); + } + foreach ($this->iterateImporterRows() as $request) { try { $product = new ProductImporterAction( @@ -52,7 +61,8 @@ public function handle() $this->user, $this->region, $this->app, - $this->runWorkflow + $this->runWorkflow, + $ownerUser )->execute(); if ($product->wasRecentlyCreated) { $created++; diff --git a/src/Domains/Inventory/Products/Actions/CreateProductAction.php b/src/Domains/Inventory/Products/Actions/CreateProductAction.php index 8b9c8f0bee..96feb04e44 100644 --- a/src/Domains/Inventory/Products/Actions/CreateProductAction.php +++ b/src/Domains/Inventory/Products/Actions/CreateProductAction.php @@ -56,7 +56,7 @@ public function execute(): Products 'warranty_terms' => $this->productDto->warranty_terms, 'upc' => $this->productDto->upc, 'status_id' => $this->productDto->status_id, - 'users_id' => $this->user->getId(), + 'users_id' => ($this->productDto->ownerUser ?? $this->user)->getId(), 'weight' => $this->productDto->weight ?? $existingProduct?->weight ?? 0, ]; @@ -97,7 +97,7 @@ public function execute(): Products } if ($this->productDto->variants) { - VariantService::createVariantsFromArray($products, $this->productDto->variants, $this->user); + VariantService::createVariantsFromArray($products, $this->productDto->variants, $this->user, $this->productDto->ownerUser); } else { VariantService::createDefaultVariant($products, $this->user, $this->productDto); } diff --git a/src/Domains/Inventory/Products/DataTransferObject/Product.php b/src/Domains/Inventory/Products/DataTransferObject/Product.php index 5f859c29dc..afc42e8e63 100644 --- a/src/Domains/Inventory/Products/DataTransferObject/Product.php +++ b/src/Domains/Inventory/Products/DataTransferObject/Product.php @@ -45,6 +45,7 @@ public function __construct( public array $files = [], public ?string $slug = null, public ?float $weight = null, + public ?UserInterface $ownerUser = null, ) { } diff --git a/src/Domains/Inventory/Variants/Actions/CreateVariantsAction.php b/src/Domains/Inventory/Variants/Actions/CreateVariantsAction.php index 5fb88c4fb1..6aef26e51f 100644 --- a/src/Domains/Inventory/Variants/Actions/CreateVariantsAction.php +++ b/src/Domains/Inventory/Variants/Actions/CreateVariantsAction.php @@ -64,7 +64,7 @@ public function execute(): Variants $search, [ 'name' => $this->variantDto->name, - 'users_id' => $this->user->getId(), + 'users_id' => ($this->variantDto->ownerUser ?? $this->user)->getId(), 'slug' => $this->variantDto->slug ?? Str::slug($this->variantDto->name), 'description' => $this->variantDto->getDescription(), 'short_description' => $this->variantDto->short_description, diff --git a/src/Domains/Inventory/Variants/Actions/UpdateVariantsAction.php b/src/Domains/Inventory/Variants/Actions/UpdateVariantsAction.php index 2b5bba0780..76c72d2927 100644 --- a/src/Domains/Inventory/Variants/Actions/UpdateVariantsAction.php +++ b/src/Domains/Inventory/Variants/Actions/UpdateVariantsAction.php @@ -59,7 +59,7 @@ public function execute(): Variants 'name' => $this->variantDto->name, 'slug' => $this->variantDto->slug ?? Str::slug($this->variantDto->name), 'sku' => $this->variantDto->sku, - 'users_id' => $this->user->getId(), + 'users_id' => ($this->variantDto->ownerUser ?? $this->user)->getId(), 'description' => $this->variantDto->getDescription() ?? $this->variant->description, 'short_description' => $this->variantDto->short_description ?? $this->variant->short_description, 'html_description' => ! empty($this->variantDto->html_description) diff --git a/src/Domains/Inventory/Variants/DataTransferObject/Variants.php b/src/Domains/Inventory/Variants/DataTransferObject/Variants.php index 59e462acc4..8ca0c4b9bb 100644 --- a/src/Domains/Inventory/Variants/DataTransferObject/Variants.php +++ b/src/Domains/Inventory/Variants/DataTransferObject/Variants.php @@ -26,7 +26,8 @@ public function __construct( public ?string $slug = null, public array $files = [], public ?float $weight = null, - public ?bool $is_published = true + public ?bool $is_published = true, + public ?UserInterface $ownerUser = null ) { } diff --git a/src/Domains/Inventory/Variants/Services/VariantService.php b/src/Domains/Inventory/Variants/Services/VariantService.php index 738bc909b5..da36b5eecd 100644 --- a/src/Domains/Inventory/Variants/Services/VariantService.php +++ b/src/Domains/Inventory/Variants/Services/VariantService.php @@ -38,8 +38,12 @@ class VariantService /** * Create a new product variants. */ - public static function createVariantsFromArray(Products $product, array $variants, UserInterface $user): array - { + public static function createVariantsFromArray( + Products $product, + array $variants, + UserInterface $user, + ?UserInterface $ownerUser = null + ): array { $variantsData = []; foreach ($variants as $variant) { @@ -54,6 +58,7 @@ public static function createVariantsFromArray(Products $product, array $variant 'product' => $product, 'products_id' => $product->getId(), ...$variant, + 'ownerUser' => $ownerUser, ]); $existVariantUpdate = Variants::fromCompany($product->company)->fromApp($product->app)->where('sku', $variantDto->sku); @@ -166,6 +171,7 @@ public static function createDefaultVariant(Products $product, UserInterface $us 'product' => $product, 'products_id' => $product->getId(), ...$variant, + 'ownerUser' => $productDto?->ownerUser, ]); $variantModel = (new CreateVariantsAction($variantDto, $user))->execute(); diff --git a/tests/Inventory/Integration/Imports/ProductImporterOwnerTest.php b/tests/Inventory/Integration/Imports/ProductImporterOwnerTest.php new file mode 100644 index 0000000000..31eab926c1 --- /dev/null +++ b/tests/Inventory/Integration/Imports/ProductImporterOwnerTest.php @@ -0,0 +1,180 @@ +user(); + $app = app(Apps::class); + $company = $user->getCurrentCompany(); + $region = $this->makeRegion($user, $company, $app); + $this->makeWarehouse($user, $company, $app, $region); + $productType = $this->makeProductType($user, $company); + + $owner = Users::factory()->create(); + $slug = 'owner-prod-' . uniqid(); + $sku = 'OWNER-SKU-' . uniqid(); + + new ProductImporterAction( + ProductImporter::from([ + 'name' => 'Owner Product', + 'slug' => $slug, + 'sku' => $sku, + 'description' => 'Owned by a different user', + 'variants' => [ + ['name' => 'Owner Variant', 'sku' => $sku, 'price' => 10.0, 'quantity' => 5], + ], + 'productType' => ['name' => $productType->name, 'weight' => 1], + ]), + $company, + $user, + $region, + $app, + false, + $owner, + )->execute(); + + $product = Products::where('slug', $slug) + ->where('apps_id', $app->getId()) + ->where('companies_id', $company->getId()) + ->first(); + + $this->assertNotNull($product); + $this->assertSame( + $owner->getId(), + (int) $product->users_id, + 'Product users_id must be the selected owner, not the importer', + ); + $this->assertTrue( + (bool) $product->is_published, + 'Importer is admin, so the product still publishes even though the owner is a regular user', + ); + + $variants = $product->variants()->get(); + $this->assertGreaterThan(0, $variants->count()); + foreach ($variants as $variant) { + $this->assertSame( + $owner->getId(), + (int) $variant->users_id, + 'Each variant users_id must be the selected owner', + ); + } + } + + public function testImportUsesActingUserWhenNoOwnerProvided(): void + { + $user = auth()->user(); + $app = app(Apps::class); + $company = $user->getCurrentCompany(); + $region = $this->makeRegion($user, $company, $app); + $this->makeWarehouse($user, $company, $app, $region); + $productType = $this->makeProductType($user, $company); + + $slug = 'default-owner-prod-' . uniqid(); + $sku = 'DEFAULT-SKU-' . uniqid(); + + new ProductImporterAction( + ProductImporter::from([ + 'name' => 'Default Owner Product', + 'slug' => $slug, + 'sku' => $sku, + 'variants' => [ + ['name' => 'Default Variant', 'sku' => $sku, 'price' => 10.0, 'quantity' => 5], + ], + 'productType' => ['name' => $productType->name, 'weight' => 1], + ]), + $company, + $user, + $region, + $app, + false, + )->execute(); + + $product = Products::where('slug', $slug) + ->where('apps_id', $app->getId()) + ->where('companies_id', $company->getId()) + ->first(); + + $this->assertNotNull($product); + $this->assertSame( + $user->getId(), + (int) $product->users_id, + 'Without an owner the importer owns the product, exactly as before', + ); + + foreach ($product->variants()->get() as $variant) { + $this->assertSame($user->getId(), (int) $variant->users_id); + } + } + + private function makeRegion($user, $company, $app): object + { + return new CreateRegionAction( + new Region( + $company, + $app, + $user, + Currencies::getById(1), + 'Region ' . uniqid(), + 'r-' . uniqid(), + null, + 1, + ), + $user, + )->execute(); + } + + private function makeWarehouse($user, $company, $app, $region): void + { + new CreateWarehouseAction( + new Warehouses( + $company, + $app, + $user, + $region, + 'Warehouse ' . uniqid(), + 'Test Location', + true, + true, + ), + $user, + )->execute(); + } + + private function makeProductType($user, $company): object + { + return new CreateProductTypeAction( + new ProductsTypesDto( + $company, + $user, + 'Owner Test Type ' . uniqid(), + 'Type used by the owner import test', + 1, + true, + ), + $user, + )->execute(); + } +}