Files
BarangaySystem/app/Http/Controllers/Market/StoreController.php
2026-06-06 18:43:00 +08:00

1510 lines
54 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Enums\UserActions;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\Permissions\ProductPermissions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\PhotoURL;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Http\Controllers\Helpers\UserController;
use App\Http\Controllers\UserManagement\CreateUserControllerUltimate;
use App\Http\Controllers\viewHelperController;
use App\Models\FileList;
use App\Models\Market\Organization;
use App\Models\Market\Product;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Session;
use App\Models\Market\Store;
use Hypervel\Support\Facades\Validator;
use Hypervel\Support\Facades\Config;
use Symfony\Component\HttpFoundation\Test\Constraint\ResponseHeaderLocationSame;
class StoreController
{
public function getCategories(Request|false $request = false)
{
$nonRequest = $request == false;
$categories = array_keys(Config::get('market.storesCategorySubcategory', []));
if (!$nonRequest) {
return response()->json($categories);
}
return $categories;
}
public function getSubcategories(Request|false $request = false, string|false $category = false)
{
$request = $request ?: request();
$category = $request ? $request->input('category') : $category;
$categoryList = Config::get('market.storesCategorySubcategory', []);
if (!$category || !array_key_exists($category, $categoryList)) {
if ($request) {
return response()->json([], 400);
}
return [];
}
$subcategories = $categoryList[$category] ?? [];
if ($request) {
return response()->json($subcategories);
}
return $subcategories;
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255|unique:str,name',
'description' => 'required|string',
'address' => 'required|string',
'category' => 'nullable|string|max:100',
'subcategory' => 'nullable|string|max:100',
'remarks' => 'nullable|string',
'photourl' => 'nullable|array',
'files' => 'nullable|array',
'owner' => 'nullable|string',
'manager' => 'nullable|string',
'managers' => 'nullable|array',
'cooperatives' => 'nullable|array',
'cooperatives.*' => 'string',
], [
'name.unique' => 'A store with this name already exists. Please choose a different name.',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => $validator->errors()->first(),
'errors' => $validator->errors()
], 422);
}
$validated = $validator->validated();
$storeCode = $request->input('storecode')
?? self::generateStoreCode($request->input('category', 'General'));
/** @var User $currentUser */
$currentUser = Auth::user();
if (!$currentUser) {
return ResponseHelper::returnUnauthorized();
}
$acctTypeEarly = $currentUser->acct_type instanceof UserTypes ? $currentUser->acct_type : UserTypes::tryFrom($currentUser->acct_type);
$isStoreOwner = $acctTypeEarly === UserTypes::STORE_OWNER;
// STORE_OWNER: force owner to self, ignore any client-supplied owner/manager
if ($isStoreOwner) {
$ownerId = $currentUser->id;
$managerId = $currentUser->id;
} else {
$ownerId = ($validated['owner'] ?? null) ? UserController::findUserIdByHash($validated['owner']) : null;
$managerId = ($validated['manager'] ?? null) ? UserController::findUserIdByHash($validated['manager']) : null;
}
$acctType = $acctTypeEarly;
$isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]);
// RBAC: only Big3 or STORE_OWNER may create stores
if (!$isBig3 && !$isStoreOwner) {
return response()->json(['success' => false, 'message' => 'You are not allowed to create stores.'], 403);
}
if (!$isBig3 && !$isStoreOwner && $ownerId) {
$isParent = UserPermissions::IsParentofTargetUser($validated['owner']);
if (!$isParent && Auth::id() !== $ownerId) {
return response()->json(['success' => false, 'message' => 'Unauthorized to create store for this owner'], 403);
}
}
// STORE_OWNER cannot override status — always active
$status = $isStoreOwner ? 'active' : $request->input('status', 'active');
$data = [
'storecode' => $storeCode,
'name' => $validated['name'],
'description' => $validated['description'],
'status' => $status,
'remarks' => $isStoreOwner ? '' : ($validated['remarks'] ?? ''),
'category' => $validated['category'] ?? 'General',
'subcategory' => $validated['subcategory'] ?? '',
'photourl' => $validated['photourl'] ?? $validated['files'] ?? '',
'address' => $validated['address'],
'owner_id' => $ownerId,
'manager_id' => $managerId,
'created_by' => Auth::id(),
'is_active' => true,
];
$store = Store::create($data);
// STORE_OWNER auto-self-assigns as manager
if ($isStoreOwner) {
\App\Models\Market\StoreManager::create([
'store_id' => $store->id,
'user_id' => $currentUser->id,
'created_by' => $currentUser->id,
]);
}
// Handle multiple managers. For STORE_OWNER, restrict to STORE_MANAGER descendants.
if ($request->filled('managers') && is_array($request->input('managers'))) {
$descendantIds = null;
if ($isStoreOwner) {
$descendantIds = $currentUser->getAllDescendants()->pluck('id')->toArray();
}
foreach ($request->input('managers') as $mgrHash) {
$mgrId = UserController::findUserIdByHash($mgrHash);
if (!$mgrId) {
continue;
}
if ($isStoreOwner) {
if (!in_array($mgrId, $descendantIds, true)) {
continue;
}
$mgrUser = User::find($mgrId);
$mgrType = $mgrUser?->acct_type instanceof UserTypes ? $mgrUser->acct_type : UserTypes::tryFrom($mgrUser?->acct_type ?? '');
if ($mgrType !== UserTypes::STORE_MANAGER) {
continue;
}
if ($mgrId === $currentUser->id) {
continue; // already added above
}
}
\App\Models\Market\StoreManager::create([
'store_id' => $store->id,
'user_id' => $mgrId,
'created_by' => Auth::id(),
]);
}
}
// Sync cooperative links (optional, many-to-many) — admins only
if (!$isStoreOwner) {
$this->syncStoreCooperatives($store, $request->input('cooperatives', []));
}
return Response::json(['success' => true, 'hashkey' => $store->hashkey], 201);
}
/**
* One-click store creation: assigns the current user as both owner
* and sole manager, with sensible defaults. Used by the Store Owner home.
*/
public function autoCreate(Request $request)
{
/** @var User $currentUser */
$currentUser = Auth::user();
if (!$currentUser) {
return ResponseHelper::returnUnauthorized();
}
$acctType = $currentUser->acct_type instanceof UserTypes
? $currentUser->acct_type
: UserTypes::tryFrom($currentUser->acct_type);
$isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]);
$isStoreOwner = $acctType === UserTypes::STORE_OWNER;
if (!$isBig3 && !$isStoreOwner) {
return response()->json(['success' => false, 'message' => 'You are not allowed to create stores.'], 403);
}
$displayName = $currentUser->nickname ?: ($currentUser->name ?: 'My');
$name = trim((string) $request->input('name')) ?: ($displayName . "'s Store");
$description = trim((string) $request->input('description')) ?: 'Auto-created store';
$address = trim((string) $request->input('address')) ?: 'Address not set';
$category = $request->input('category', 'General');
// `str.name` is globally unique; if the auto-generated name collides
// (two owners sharing a nickname), append a short suffix until free.
$baseName = $name;
$attempt = 0;
while (Store::where('name', $name)->exists()) {
$attempt++;
if ($attempt > 20) {
return response()->json([
'success' => false,
'message' => 'Could not generate a unique store name. Please use Custom Create.',
], 409);
}
$name = $baseName . ' #' . strtoupper(substr(bin2hex(random_bytes(2)), 0, 4));
}
$store = Store::create([
'storecode' => self::generateStoreCode($category),
'name' => $name,
'description' => $description,
'status' => 'active',
'remarks' => '',
'category' => $category,
'subcategory' => '',
'photourl' => '',
'address' => $address,
'owner_id' => $currentUser->id,
'manager_id' => $currentUser->id,
'created_by' => $currentUser->id,
'is_active' => true,
]);
\App\Models\Market\StoreManager::create([
'store_id' => $store->id,
'user_id' => $currentUser->id,
'created_by' => $currentUser->id,
]);
return Response::json([
'success' => true,
'hashkey' => $store->hashkey,
'name' => $store->name,
], 201);
}
public static function generateStoreCode(string $category, ?int $num = null, int $length = 8): string
{
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_';
$charactersLength = strlen($characters);
$num = $num ?? rand(1, 99);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
$categoryPrefix = substr($category, 0, 2);
$categorySuffix = substr($category, -2, 2);
$cat = strtoupper($categoryPrefix . $categorySuffix);
return date('Y') . '-' . $cat . '-' . $num . '-' . $randomString;
}
public static function viewStoreDetailsLegacy(Request|false $request = false, string|false $target = false)
{
try {
if ($target) {
$hash = $target;
} else {
$hash = $request->input('target');
}
if (!$hash) {
return $target ? false : response()->json(['success' => false]);
}
$store = Store::where('hashkey', $hash)->first();
if (!$store) {
return $target ? false : response()->json(['success' => false]);
}
$products = Product::where('store_id', $store->id)
->select('name', 'hashkey', 'price', 'unitname', 'available', 'is_active as status', 'photourl')
->get()
->filter(fn($product) => $product->available)
->map(function ($product) {
$photourl = $product->photourl;
if ($photourl && (is_array($photourl) || is_object($photourl))) {
$product->photourl = (array)$photourl;
} elseif (is_string($photourl)) {
$product->photourl = json_decode($photourl, true) ?: [];
} else {
$product->photourl = [];
}
return $product;
})
->values();
$store->products = $products;
if ($target) {
return $store;
} else {
return response()->json(['success' => true, 'data' => $store]);
}
} catch (\Exception $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()], 500);
}
}
public static function viewStoreDetails(Request|false $request = false, string|false $target = false)
{
$req = $request ?: request();
$hash = $target ?: ($req?->input('target') ?? null);
if (!$hash) {
return $target ? false : response()->json(['success' => false]);
}
// Fetch the store by hashkey
$store = Store::where('hashkey', $hash)
->select(
'id',
'photourl',
'hashkey',
'name',
'category',
'subcategory',
'created_at as created',
'updated_at as modified',
'description',
'is_active as status',
'address',
'owner_id',
'manager_id'
)
->first();
if (!$store) {
return $target ? false : response()->json(['success' => false]);
}
/** @var User $user */
$user = Auth::user();
$products = $store->products()
->select(
'prd_items.name',
'prd_items.hashkey',
'prd_str.price as store_price',
'prd_items.price',
'prd_items.unitname',
'prd_str.available',
'prd_str.is_active as status',
'prd_items.photourl'
)
->get()
//TODO Decide later wheether to include unavailable store products
// ->filter(fn($product) => $product->available)
->map(function ($product) {
$photourl = $product->photourl;
if ($photourl && is_array($photourl)) {
$product->photourl = $photourl;
} elseif (is_string($photourl)) {
$product->photourl = json_decode($photourl, true) ?: [];
} else {
$product->photourl = [];
}
return $product;
})
->values();
$store->products = $products;
// Resolve store photo URLs: prefer cdn_url, fall back to /RequestData/File/{hash}
$rawPhotos = $store->photourl ?? [];
if (!empty($rawPhotos) && is_array($rawPhotos)) {
$store->resolved_photos = \App\Models\FileList::whereIn('hashkey', $rawPhotos)
->get(['hashkey', 'cdn_url'])
->keyBy('hashkey')
->pipe(function ($fileMap) use ($rawPhotos) {
return collect($rawPhotos)->map(function ($hash) use ($fileMap) {
$file = $fileMap->get($hash);
if (!$file) return null;
$cdn = trim((string)($file->cdn_url ?? ''));
return $cdn !== '' ? $cdn : "/RequestData/File/{$hash}";
})->filter()->values()->toArray();
});
} else {
$store->resolved_photos = [];
}
// Check if user can add products to this store
$store->can_add_product = ProductPermissions::isActionAllowed(UserActions::AddProducttoOwnStore, null, $store)
|| ProductPermissions::isActionAllowed(UserActions::AddProducttoAnyStore, null, $store);
// Check if user can assign existing products to this store
$store->can_assign_product = self::canUserAssignProduct($store);
// Check if user can access POS for this store
$store->can_access_pos = self::canUserAccessPos($store);
// Return the formatted response
return $target ? $store : response()->json(['success' => true, 'data' => $store]);
}
/**
* Determine if the current user can assign existing products to a given store.
* Mirrors the permission check in ProductController::AssignProductToOwnStore.
*/
private static function canUserAssignProduct(Store $store): bool
{
/** @var User $user */
$user = Auth::user();
if (!$user) {
return false;
}
$acctType = $user->acct_type->value ?? $user->acct_type ?? '';
if (in_array($acctType, ['ult', 'super operator', 'operator'])) {
return true;
}
if ($store->owner_id === $user->id || $store->manager_id === $user->id) {
return true;
}
if ($store->managers()->where('user_id', $user->id)->exists()) {
return true;
}
if (UserPermissions::isDescendantOfCurrentUser($store->owner_id)) {
return true;
}
$managerIds = $store->managerUsers()->pluck('users.id')->toArray();
foreach ($managerIds as $managerId) {
if (UserPermissions::isDescendantOfCurrentUser($managerId)) {
return true;
}
}
return false;
}
/**
* Determine if the current user can access POS for a given store.
* Returns true if:
* - User is the store owner or manager
* - User is a direct/indirect parent of the store owner (via parentuid chain)
* - User has an admin role (ult, super operator, operator)
*/
private static function canUserAccessPos(Store $store): bool
{
/** @var User $user */
$user = Auth::user();
if (!$user) {
return false;
}
$acctType = $user->acct_type->value ?? $user->acct_type ?? '';
// Big 3 always have access
if (in_array($acctType, ['ult', 'super operator', 'operator'])) {
return true;
}
// Store owner
if ($store->owner_id === $user->id) {
return true;
}
// Check if user is a store manager (primary or in the list)
if ($store->manager_id === $user->id) {
return true;
}
if ($store->managers()->where('user_id', $user->id)->exists()) {
return true;
}
// Check hierarchy: parent of owner or any manager
if (UserPermissions::isDescendantOfCurrentUser($store->owner_id)) {
return true;
}
$managerIds = $store->managerUsers()->pluck('users.id')->toArray();
foreach ($managerIds as $managerId) {
if (UserPermissions::isDescendantOfCurrentUser($managerId)) {
return true;
}
}
return false;
}
public static function editStoreDetails(Request|false $request = false, string|false $target = false)
{
$req = $request ?: request();
$hash = $target ?: ($req?->input('target') ?? null);
// file_put_contents('Storedetails.txt',$hash);
if (!$hash) {
return $target ? false : response()->json(['success' => false]);
}
$store = Store::where('hashkey', $hash)
->select(
'photourl',
'hashkey',
'name',
'category',
'subcategory',
'created_at as created',
'updated_at as modified',
'description',
'photourl',
'status',
'is_active',
'remarks',
'address',
'owner_id',
'manager_id',
)->with('owner:hashkey,id')
->with('manager:hashkey,id')
->first();
if (!$store) {
return $target ? false : response()->json(['success' => false]);
}
try {
$store->owner_hashkey = $store->owner->hashkey;
} catch (\Exception $th) {
$store->owner_hashkey = null;
}
try {
$store->manager_hashkey = $store->manager->hashkey;
} catch (\Throwable $th) {
$store->manager_hashkey = 0;
}
try {
$store->photourlDropzone = PhotoURL::photoURLArraytoDropzoneData($store->photourl);
} catch (\Throwable $th) {
$store->photourlDropzone = [];
}
try {
$store->ParentList = CreateUserControllerUltimate::listAllUsersforParentSelectHTML(request(), true);
} catch (\Exception $e) {
$store->ParentList = [];
}
// Include multi-manager hashkeys for frontend
try {
$store->managers_hashkeys = $store->managerUsers()->pluck('users.hashkey')->toArray();
} catch (\Throwable $th) {
$store->managers_hashkeys = [];
}
// Include linked cooperative hashkeys
try {
$store->cooperative_hashkeys = $store->cooperatives()->pluck('organizations.hashkey')->toArray();
} catch (\Throwable $th) {
$store->cooperative_hashkeys = [];
}
$products = $store->products()
->select(
'prd_items.name',
'prd_items.hashkey',
'prd_str.price',
'prd_items.unitname',
'prd_str.available',
'prd_str.is_active as status',
'prd_items.photourl'
)
->get()
->filter(fn($product) => $product->available)
->map(function ($product) {
$photourl = $product->photourl;
if ($photourl && is_array($photourl)) {
$product->photourl = $photourl;
} elseif (is_string($photourl)) {
$product->photourl = json_decode($photourl, true) ?: [];
} else {
$product->photourl = [];
}
return $product;
})
->values();
return $target ? $store : response()->json($store);
}
public function update(Request $request)
{
$hashkey = request()->input('target');
$store = Store::where('hashkey', $hashkey)->first();
if (!$store) {
return response()->json([
'success' => false,
'message' => 'Store not found.'
], 404);
}
/** @var User $user */
$user = Auth::user();
if (!$user) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$acctType = $user->acct_type instanceof UserTypes ? $user->acct_type : UserTypes::tryFrom($user->acct_type);
$isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]);
// Permission check: Big 3, parent of owner, direct owner, direct manager, or pivot manager
$isParentOfOwner = UserPermissions::isDescendantOfCurrentUser($store->owner_id);
$isDirectManager = $store->manager_id === $user->id;
$isManagerViaPivot = $store->managers()->where('user_id', $user->id)->exists();
if (!$isBig3 && !$isParentOfOwner && $store->owner_id !== $user->id && !$isDirectManager && !$isManagerViaPivot) {
return response()->json(['error' => 'Unauthorized to modify this store'], 403);
}
$storeId = $store->id;
$validator = Validator::make($request->all(), [
'name' => [
'required', 'string', 'max:255',
function ($attribute, $value, $fail) use ($storeId) {
$conflict = \App\Models\Market\Store::where('name', $value)
->where('id', '!=', $storeId)
->exists();
if ($conflict) {
$fail('A store with this name already exists. Please choose a different name.');
}
},
],
'description' => 'required|string',
'address' => 'required|string',
'category' => 'nullable|string|max:100',
'subcategory' => 'nullable|string|max:100',
'remarks' => 'nullable|string',
'photourl' => 'nullable|array',
'files' => 'nullable|array',
'owner' => 'nullable|string',
'manager' => 'nullable|string',
'managers' => 'nullable|array',
'cooperatives' => 'nullable|array',
'cooperatives.*' => 'string',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
$validated = $validator->validated();
$ownerId = ($validated['owner'] ?? null) ? UserController::findUserIdByHash($validated['owner']) : $store->owner_id;
$managerId = ($validated['manager'] ?? null) ? UserController::findUserIdByHash($validated['manager']) : $store->manager_id;
$status = $request->input('status', $store->status ?? 'active');
$data = [
'name' => $validated['name'],
'description' => $validated['description'],
'address' => $validated['address'],
'remarks' => $validated['remarks'] ?? $store->remarks,
'category' => $validated['category'] ?? $store->category,
'subcategory' => $validated['subcategory'] ?? $store->subcategory,
'status' => $status,
'photourl' => $validated['photourl'] ?? $validated['files'] ?? $store->photourl,
'owner_id' => $ownerId,
'manager_id' => $managerId,
'updated_by' => Auth::id(),
];
$store->update($data);
// Handle multiple managers
if ($request->filled('managers') && is_array($request->input('managers'))) {
// Remove existing managers not in the new list
$newMgrIds = [];
foreach ($request->input('managers') as $mgrHash) {
$mgrId = UserController::findUserIdByHash($mgrHash);
if ($mgrId) $newMgrIds[] = $mgrId;
}
$store->managers()->whereNotIn('user_id', $newMgrIds)->delete();
// Add new ones
foreach ($newMgrIds as $mgrId) {
if (!$store->managers()->where('user_id', $mgrId)->exists()) {
\App\Models\Market\StoreManager::create([
'store_id' => $store->id,
'user_id' => $mgrId,
'created_by' => Auth::id(),
]);
}
}
}
// Sync cooperative links (only when key is present, allows clearing with empty array)
if ($request->has('cooperatives')) {
$this->syncStoreCooperatives($store, $request->input('cooperatives', []), true);
}
return response()->json([
'success' => true,
'message' => 'Store updated successfully.',
'hashkey' => $store->hashkey,
], 200);
}
/**
* Attach a store to a product with optional pivot data.
*
* This creates (or updates) a relationship between a given Product and Store
* in the `product_store` pivot table. It allows specifying stock quantity,
* price, and active status for that store-product link.
*
* If either the product or store cannot be found, the method safely returns false.
*
* @param \App\Models\Market\Product|int $product The Product model instance or its ID.
* @param \App\Models\Market\Store|int $store The Store model instance or its ID.
* @param int $available Quantity available for this product in the store (default: 0)
* @param int $price Price for this product in the store (default: 0)
* @param bool $is_active Whether the product is active in this store (default: true)
* @return bool Returns true on success, false if product/store not found or an exception occurs.
*
* @example
* ```php
* // Attach store #5 to product #12 with stock and price
* Product::attachStoreToProduct(12, 5, available: 40, price: 1499, is_active: true);
*
* // Using model instances
* $product = Product::find(12);
* $store = Store::find(5);
* Product::attachStoreToProduct($product, $store, 25, 1299);
* ```
*/
public static function attachStoreToProduct(Product|int $product, Store|int $store, int $available = 0, int $price = 0, bool $is_active = true)
{
if (is_int($product)) {
try {
$product = Product::findOrFail($product);
} catch (\Throwable $th) {
return false;
}
}
if (is_int($store)) {
try {
$store = Store::findOrFail($store);
} catch (\Throwable $th) {
return false;
}
}
try {
$product->stores()->syncWithoutDetaching([
$store->id => [
'available' => $available ?? 0,
'price' => $price ?? 0,
'is_active' => $is_active,
]
]);
return true;
} catch (\Throwable $th) {
return false;
}
}
/**
* Attach a product to a store with optional pivot data.
*
* This creates (or updates) a relationship between a given Store and Product
* in the `product_store` pivot table. You can specify availability, price,
* and activation status as pivot attributes.
*
* If either the store or product is not found, the method safely returns false.
*
* @param \App\Models\Market\Store|int $store The Store model instance or its ID.
* @param \App\Models\Market\Product|int $product The Product model instance or its ID.
* @param int $available Quantity available (default: 0)
* @param int $price Product price (default: 0)
* @param bool $is_active Whether the product is active in this store (default: true)
* @return bool Returns true on success, false if store/product not found or an exception occurs.
*
* @example
* ```php
* // Attach product #10 to store #3
* Store::attachProductToStore(3, 10, 25, 1999, true);
*
* // Using model instances
* $store = Store::find(3);
* $product = Product::find(10);
* Store::attachProductToStore($store, $product, available: 100, price: 2499);
* ```
*/
public static function attachProducttoStore(Store|int $store, Product|int $product, int $available = 0, int $price = 0, bool $is_active = true)
{
if (is_int($store)) {
try {
$store = Store::findOrFail($store);
} catch (\Throwable $th) {
return false;
}
}
if (is_int($product)) {
try {
$product = Product::findOrFail($product);
} catch (\Throwable $th) {
return false;
}
}
$store->products()->attach($product->id, ['available' => $available ?? 0, 'price' => $price ?? 0, 'is_active' => $is_active,]);
return true;
}
/**
* Detach (unlink) a product from a store.
*
* This removes the relationship between a given Store and Product
* from the `product_store` pivot table, without deleting either record.
*
* If the link does not exist, this method silently succeeds (no error is thrown).
* If either the store or product cannot be found, the method safely returns false.
*
* @param \App\Models\Market\Store|int $store The Store model instance or its ID.
* @param \App\Models\Market\Product|int $product The Product model instance or its ID.
* @return bool Returns true on success, false if store/product not found or an exception occurs.
*
* @example
* ```php
* // Detach product #10 from store #3
* Store::detachProductFromStore(3, 10);
*
* // Using model instances
* $store = Store::find(3);
* $product = Product::find(10);
* Store::detachProductFromStore($store, $product);
* ```
*/
public static function detachProductFromStore(Store|int $store, Product|int $product)
{
// Resolve Store
if (is_int($store)) {
try {
$store = Store::findOrFail($store);
} catch (\Throwable $th) {
return false;
}
}
// Resolve Product
if (is_int($product)) {
try {
$product = Product::findOrFail($product);
} catch (\Throwable $th) {
return false;
}
}
// Attempt to detach
try {
$store->products()->detach($product->id);
return true;
} catch (\Throwable $th) {
return false;
}
}
/**
* Detach (unlink) a store from a product.
*
* This removes the relationship between a given Product and Store
* from the `product_store` pivot table, without deleting either record.
*
* If the relationship doesnt exist, the operation succeeds silently.
* If either the product or store is not found, the method safely returns false.
*
* @param \App\Models\Market\Product|int $product The Product model instance or its ID.
* @param \App\Models\Market\Store|int $store The Store model instance or its ID.
* @return bool Returns true on success, false if product/store not found or an exception occurs.
*
* @example
* ```php
* // Detach store #2 from product #7
* Product::detachStoreFromProduct(7, 2);
*
* // Using model instances
* $product = Product::find(7);
* $store = Store::find(2);
* Product::detachStoreFromProduct($product, $store);
* ```
*/
public static function detachStoreFromProduct(Product|int|string $product, Store|int|string $store)
{
if (is_int($product)) {
try {
$product = Product::findOrFail($product);
} catch (\Throwable $th) {
$product = null;
}
}
if (is_int($store)) {
try {
$store = Store::findOrFail($store);
} catch (\Throwable $th) {
$store = null;
}
}
if (is_string($product)) {
try {
$product = Product::where('hashkey', $product)->first();
} catch (\Throwable $th) {
$store = null;
}
}
if (is_string($store)) {
try {
$store = Store::where('hashkey', $store)->first();
} catch (\Throwable $th) {
$store = null;
}
}
if (!$store || !$product) {
return false;
}
// Attempt to detach
try {
$product->stores()->detach($store->id);
return true;
} catch (\Throwable $th) {
return false;
}
}
/**
* Synchronize (replace) all products attached to a given store.
*
* This method accepts either a Store model instance or a store ID.
* It replaces the store's entire product list in the `product_store` pivot table
* with the provided data array.
*
* Existing product links not included in the `$products` array will be detached.
* New links will be created, and existing ones will be updated.
*
* @param \App\Models\Market\Store|int $store The Store model instance or its ID.
* @param array $products Associative array of product IDs and pivot attributes.
* Format:
* [
* product_id => [
* 'available' => (int),
* 'price' => (int),
* 'is_active' => (bool),
* ],
* ...
* ]
* @return bool Returns true on success, false if the store is not found or an exception occurs.
*
* @example
* ```php
* // Replace all products for store #1
* Store::syncProductsToStore(1, [
* 10 => ['available' => 50, 'price' => 1499, 'is_active' => true],
* 11 => ['available' => 20, 'price' => 1999, 'is_active' => false],
* ]);
*
* // Using a Store instance
* $store = Store::find(1);
* Store::syncProductsToStore($store, [
* 5 => ['available' => 100, 'price' => 2999, 'is_active' => true],
* ]);
* ```
*/
public static function syncProductsToStore(Store|int $store, array $products)
{
// $products format: [product_id => ['available' => ..., 'price' => ..., 'is_active' => ...]]
// Resolve store
if (is_int($store)) {
try {
$store = Store::findOrFail($store);
} catch (\Throwable $th) {
return false;
}
}
// Validate array structure (optional)
if (!is_array($products) || empty($products)) {
return false;
}
try {
// Replace all existing product links
$store->products()->sync($products);
return true;
} catch (\Throwable $th) {
return false;
}
}
/**
* Synchronize (replace) all stores attached to a given product.
*
* This method accepts either a Product model instance or a product ID.
* It replaces the product's entire store list in the `product_store` pivot table
* with the provided data array.
*
* Existing store links not listed in the `$stores` array will be detached.
* New ones will be created, and existing ones will be updated.
*
* @param \App\Models\Market\Product|int $product The Product model instance or its ID.
* @param array $stores Associative array of store IDs and pivot attributes.
* Format:
* [
* store_id => [
* 'available' => (int),
* 'price' => (int),
* 'is_active' => (bool),
* ],
* ...
* ]
* @return bool Returns true on success, false if the product is not found or an exception occurs.
*
* @example
* ```php
* // Replace all stores linked to product #5
* Product::syncStoresToProduct(5, [
* 1 => ['available' => 30, 'price' => 1799, 'is_active' => true],
* 2 => ['available' => 15, 'price' => 1999, 'is_active' => false],
* ]);
*
* // Using a Product instance
* $product = Product::find(5);
* Product::syncStoresToProduct($product, [
* 3 => ['available' => 40, 'price' => 1499, 'is_active' => true],
* ]);
* ```
*/
public static function syncStoresToProduct(Product|int $product, array $stores)
{
// $stores format: [store_id => ['available' => ..., 'price' => ..., 'is_active' => ...]]
// Resolve product
if (is_int($product)) {
try {
$product = Product::findOrFail($product);
} catch (\Throwable $th) {
return false;
}
}
if (!is_array($stores) || empty($stores)) {
return false;
}
try {
// Replace all existing store links
$product->stores()->sync($stores);
return true;
} catch (\Throwable $th) {
return false;
}
}
public function listStoresActiveDataAll()
{
$stores = Store::where('status', 'active')
->select('hashkey', 'photourl', 'name', 'category', 'subcategory')
->get()
->map(function ($store) {
if (is_array($store->photourl)) {
$photoUrl = $store->photourl;
} elseif (is_string($store->photourl)) {
$photoUrl = json_decode($store->photourl, true) ?: [];
} else {
$photoUrl = [];
}
if (is_array($photoUrl) && !empty($photoUrl)) {
$photoUrl = $photoUrl[0];
} else {
$photoUrl = null;
}
$store->photourl = $photoUrl;
return $store;
});
return $stores;
}
/**
* List stores the current user is authorized to manage.
* - Ultimate users: all stores
* - Store owners: stores they own
* - Store managers: stores they manage
* Returns a JSON array of stores with hashkey, name, and role.
*/
public function listStoresForCurrentUser()
{
$user = Auth::user();
if (!$user) {
return Response::json([]);
}
$isUltimate = $user->acct_type === UserTypes::ULTIMATE;
$isSuperOperator = $user->acct_type === UserTypes::SUPER_OPERATOR;
$isOperator = $user->acct_type === UserTypes::OPERATOR;
if ($isUltimate) {
// Ultimate can see all active stores
$stores = Store::where('is_active', true)
->select(['hashkey', 'name', 'category', 'owner_id', 'manager_id'])
->get();
} elseif ($isSuperOperator || $isOperator) {
// Super Operator and Operator see their own and descendants' stores
$descendants = $user->getAllDescendants();
$allowedIds = $descendants->pluck('id')->push($user->id)->toArray();
$stores = Store::where('is_active', true)
->where(function($query) use ($allowedIds) {
$query->whereIn('owner_id', $allowedIds)
->orWhereIn('manager_id', $allowedIds);
})
->select(['hashkey', 'name', 'category', 'owner_id', 'manager_id'])
->get();
} else {
// Non-admin users: only stores they own or manage (including store_managers pivot)
$stores = Store::where('is_active', true)
->where(function($query) use ($user) {
$query->where('owner_id', $user->id)
->orWhere('manager_id', $user->id)
->orWhereHas('managers', function ($mq) use ($user) {
$mq->where('user_id', $user->id);
});
})
->select(['hashkey', 'name', 'category', 'owner_id', 'manager_id'])
->get();
}
$formattedStores = $stores->map(function ($store) use ($user) {
$role = 'viewer';
if ($store->owner_id === $user->id) {
$role = 'owner';
} elseif ($store->manager_id === $user->id) {
$role = 'manager';
} elseif ($store->managers()->where('user_id', $user->id)->exists()) {
$role = 'manager';
} elseif ($user->isUltimate() || $user->isSuperOperator() || $user->isOperator()) {
$role = 'admin';
}
return [
'hashkey' => $store->hashkey,
'name' => $store->name,
'category' => $store->category,
'role' => $role,
];
});
return Response::json($formattedStores);
}
public function removeProductfromStore()
{
//TODO fix permissions here
$product = request()->input('product_hash');
$store = request()->input('store_hash');
$isUlt = Auth::user()->acct_type === UserTypes::ULTIMATE;
if ($isUlt) {
$allowed = ProductPermissions::isActionAllowed(UserActions::AddProducttoAnyStore, $product, $store);
} else {
$allowed = ProductPermissions::isActionAllowed(UserActions::AddProducttoOwnStore, $product, $store);
}
if (!$allowed) {
return ResponseHelper::returnUnauthorized();
}
$go = self::detachStoreFromProduct($product, $store);
if ($go === true) {
$datareturn = [
'store_hash' => $store,
'product_hash' => $product,
];
return ResponseHelper::returnSuccessResponse($datareturn, $product, '');
} else {
return ResponseHelper::returnError($go);
}
}
public function listSelectableStoresForAddingProduct()
{
/** @var \App\Models\User $user */
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
$acctType = $user->acct_type instanceof UserTypes ? $user->acct_type : UserTypes::tryFrom($user->acct_type);
$isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]);
if ($isBig3) {
// Big 3 can see all stores
$stores = Store::select(['hashkey', 'name', 'category', 'owner_id', 'manager_id'])
->get();
} else {
// Include self + all descendants so a store owner sees their own stores.
$allowedUserIds = array_merge([$user->id], $user->getAllDescendants()->pluck('id')->toArray());
$stores = Store::where(function ($q) use ($allowedUserIds) {
$q->whereIn('owner_id', $allowedUserIds)
->orWhereIn('manager_id', $allowedUserIds)
->orWhereHas('managers', function ($mq) use ($allowedUserIds) {
$mq->whereIn('user_id', $allowedUserIds);
});
})
->select(['hashkey', 'name', 'category', 'owner_id', 'manager_id'])
->get();
}
$formattedStores = $stores->map(function ($store) use ($user) {
$role = 'admin';
if ($store->owner_id === $user->id) {
$role = 'owner';
} elseif ($store->manager_id === $user->id) {
$role = 'manager';
}
return [
'hashkey' => $store->hashkey,
'name' => $store->name,
'category' => $store->category,
'role' => $role,
];
});
return response()->json([
'success' => true,
'data' => $formattedStores,
]);
}
public function listStores_Admin(Request $request)
{
try {
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
$acctType = $user->acct_type instanceof UserTypes ? $user->acct_type : UserTypes::tryFrom($user->acct_type);
$isBig3 = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR]);
$columns = ['id', 'hashkey', 'name', 'category', 'subcategory', 'is_active', 'status', 'photourl', 'address', 'owner_id', 'manager_id'];
if ($isBig3) {
$stores = Store::with(['cooperatives:organizations.id,organizations.hashkey,organizations.name'])
->select($columns)
->orderBy('id', 'desc')
->get()
->each(fn($s) => $s->setAttribute('user_can_manage', true));
} else {
$allowedUserIds = array_merge([$user->id], $user->getAllDescendants()->pluck('id')->toArray());
$stores = Store::with(['cooperatives:organizations.id,organizations.hashkey,organizations.name', 'managers:id,store_id,user_id'])
->select($columns)
->where(function ($q) use ($allowedUserIds) {
$q->whereIn('owner_id', $allowedUserIds)
->orWhereIn('manager_id', $allowedUserIds)
->orWhereHas('managers', function ($mq) use ($allowedUserIds) {
$mq->whereIn('user_id', $allowedUserIds);
});
})
->orderBy('id', 'desc')
->get()
->each(function ($s) use ($allowedUserIds) {
$s->setAttribute('user_can_manage',
in_array($s->owner_id, $allowedUserIds)
|| in_array($s->manager_id, $allowedUserIds)
|| $s->managers->pluck('user_id')->intersect($allowedUserIds)->isNotEmpty()
);
unset($s->managers);
});
}
return response()->json([
'success' => true,
'stores' => $stores
]);
} catch (\Throwable $e) {
\Hypervel\Support\Facades\Log::error('listStores_Admin failed', [
'user_id' => Auth::id(),
'message' => $e->getMessage(),
'file' => $e->getFile() . ':' . $e->getLine(),
]);
return response()->json([
'success' => false,
'message' => 'Failed to load stores: ' . $e->getMessage(),
], 500);
}
}
/**
* Resolve cooperative hashkeys to ids (filtering to type=COOPERATIVE)
* and sync onto the store. When $forceReplace is false and the input
* array is empty, this is a no-op (used on create).
*/
private function syncStoreCooperatives(Store $store, $coopHashes, bool $forceReplace = false): void
{
if (!is_array($coopHashes)) {
$coopHashes = [];
}
if (!$forceReplace && empty($coopHashes)) {
return;
}
$coopIds = empty($coopHashes)
? []
: Organization::whereIn('hashkey', $coopHashes)
->where('type', 'COOPERATIVE')
->pluck('id')
->toArray();
$store->cooperatives()->sync($coopIds);
}
/**
* Lightweight cooperative list for the store create/edit pickers.
* Returns only active organizations of type COOPERATIVE.
*/
public function listCooperativesForStore(Request $request)
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
$coops = Organization::where('is_active', true)
->where('type', 'COOPERATIVE')
->select(['hashkey', 'name', 'cooperative_type', 'address'])
->orderBy('name')
->get();
return response()->json([
'success' => true,
'data' => $coops,
]);
}
public function deleteStore_Admin(Request $request)
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
$hash = $request->input('target');
if (!$hash) {
return ResponseHelper::returnIncorrectDetails();
}
try {
$store = Store::where('hashkey', $hash)->firstOrFail();
// Check permissions: Ultimate or Owner/Manager (or descendant thereof, or pivot manager)
$isUltimate = $user->acct_type === UserTypes::ULTIMATE;
if (!$isUltimate) {
$descendants = $user->getAllDescendants();
$descendantIds = $descendants->pluck('id')->toArray();
$allowedIds = array_merge([$user->id], $descendantIds);
$isManagerViaPivot = $store->managers()->whereIn('user_id', $allowedIds)->exists();
if (!in_array($store->owner_id, $allowedIds) && !in_array($store->manager_id, $allowedIds) && !$isManagerViaPivot) {
return ResponseHelper::returnUnauthorized();
}
}
$store->delete();
return ResponseHelper::returnSuccessResponse([], $hash, 'Store deleted successfully');
} catch (\Exception $e) {
return ResponseHelper::returnError('Failed to delete store: ' . $e->getMessage());
}
}
public function toggleStoreStatus_Admin(Request $request)
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
$hash = $request->input('target');
if (!$hash) {
return ResponseHelper::returnIncorrectDetails();
}
try {
$store = Store::where('hashkey', $hash)->firstOrFail();
// Check permissions: Ultimate or Owner/Manager (or descendant thereof, or pivot manager)
$isUltimate = $user->acct_type === UserTypes::ULTIMATE;
if (!$isUltimate) {
$descendants = $user->getAllDescendants();
$descendantIds = $descendants->pluck('id')->toArray();
$allowedIds = array_merge([$user->id], $descendantIds);
$isManagerViaPivot = $store->managers()->whereIn('user_id', $allowedIds)->exists();
if (!in_array($store->owner_id, $allowedIds) && !in_array($store->manager_id, $allowedIds) && !$isManagerViaPivot) {
return ResponseHelper::returnUnauthorized();
}
}
$store->is_active = !$store->is_active;
$store->save();
return ResponseHelper::returnSuccessResponse(
['is_active' => $store->is_active],
$hash,
'Store status updated successfully'
);
} catch (\Exception $e) {
return ResponseHelper::returnError('Failed to update store status: ' . $e->getMessage());
}
}
}