initial: bootstrap from BukidBountyApp base
This commit is contained in:
577
app/Http/Controllers/Market/BatchController.php
Normal file
577
app/Http/Controllers/Market/BatchController.php
Normal file
@@ -0,0 +1,577 @@
|
||||
<?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\Permissions\UserTypeService;
|
||||
use App\Http\Controllers\Helpers\ResponseHelper;
|
||||
use App\Http\Controllers\Helpers\UserController;
|
||||
use App\Models\Market\CooperativeMember;
|
||||
use App\Models\Market\Organization;
|
||||
use App\Models\Market\Product;
|
||||
use App\Models\Market\Store;
|
||||
use App\Models\User;
|
||||
use Hypervel\Support\Str;
|
||||
use Hypervel\Http\Request;
|
||||
use Hypervel\Support\Facades\Auth;
|
||||
use Hypervel\Support\Facades\DB;
|
||||
use Hypervel\Support\Facades\Hash;
|
||||
use Hypervel\Support\Facades\Response;
|
||||
use Hypervel\Support\Facades\Validator;
|
||||
|
||||
class BatchController
|
||||
{
|
||||
/**
|
||||
* Batch create products.
|
||||
* Available for Ultimate, Super Operator, and Operator.
|
||||
*/
|
||||
public function batchCreateProducts(Request $request)
|
||||
{
|
||||
$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]);
|
||||
$isStoreOwner = $acctType === UserTypes::STORE_OWNER;
|
||||
|
||||
if (!$isBig3 && !$isStoreOwner) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$products = $request->input('products', []);
|
||||
if (empty($products)) {
|
||||
return ResponseHelper::returnError('No products provided');
|
||||
}
|
||||
|
||||
$targetStoreHash = $request->input('target_store_hash');
|
||||
$targetStore = null;
|
||||
if ($targetStoreHash) {
|
||||
$targetStore = Store::where('hashkey', $targetStoreHash)->first();
|
||||
if (!$targetStore) {
|
||||
return ResponseHelper::returnError('Target store not found');
|
||||
}
|
||||
}
|
||||
|
||||
// STORE_OWNER must operate against a store they actually own/manage.
|
||||
// Without that, batch-import has no destination and must be refused
|
||||
// up-front so the UI doesn't silently 401 partway through.
|
||||
if (!$isBig3) {
|
||||
if (!$targetStore) {
|
||||
return ResponseHelper::returnError('You must select one of your stores before importing products.', 422);
|
||||
}
|
||||
$ownsTarget = (int) $targetStore->owner_id === (int) $user->id
|
||||
|| (int) $targetStore->manager_id === (int) $user->id
|
||||
|| $targetStore->managers()->where('user_id', $user->id)->exists();
|
||||
if (!$ownsTarget) {
|
||||
return ResponseHelper::returnError('You can only import products into stores you own.', 403);
|
||||
}
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$errors = [];
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
foreach ($products as $index => $productData) {
|
||||
$source = $productData['source'] ?? 'new';
|
||||
|
||||
if ($source === 'existing') {
|
||||
if (!$targetStore) {
|
||||
$errors[] = 'Row ' . ($index + 1) . ': A target store is required to import an existing product.';
|
||||
continue;
|
||||
}
|
||||
|
||||
$validator = Validator::make($productData, [
|
||||
'product_hash' => 'required|string',
|
||||
'price' => 'nullable|numeric|min:0',
|
||||
'available' => 'nullable|numeric',
|
||||
'description' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$errors[] = 'Row ' . ($index + 1) . ': ' . implode(', ', $validator->errors()->all());
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = Product::where('hashkey', $productData['product_hash'])->first();
|
||||
if (!$existing) {
|
||||
$errors[] = 'Row ' . ($index + 1) . ': Selected global product not found.';
|
||||
continue;
|
||||
}
|
||||
|
||||
$alreadyAttached = $targetStore->products()->where('prd_items.id', $existing->id)->exists();
|
||||
if ($alreadyAttached) {
|
||||
$errors[] = 'Row ' . ($index + 1) . ": '{$existing->name}' is already in the target store.";
|
||||
continue;
|
||||
}
|
||||
|
||||
$price = $productData['price'] ?? null;
|
||||
$price = ($price === null || $price === '' || (float) $price <= 0)
|
||||
? (float) $existing->price
|
||||
: (float) $price;
|
||||
|
||||
$description = $productData['description'] ?? '';
|
||||
if (trim((string) $description) === '') {
|
||||
$description = (string) ($existing->description ?? '');
|
||||
}
|
||||
|
||||
$available = (int) ($productData['available'] ?? 0);
|
||||
|
||||
$targetStore->products()->attach($existing->id, [
|
||||
'available' => $available,
|
||||
'price' => $price,
|
||||
'description' => $description,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$results[] = $existing->hashkey;
|
||||
continue;
|
||||
}
|
||||
|
||||
$validator = Validator::make($productData, [
|
||||
'name' => 'required|string|max:255',
|
||||
'price' => 'required|numeric|min:0',
|
||||
'available' => 'required|numeric',
|
||||
'unitname' => 'required|string|max:100',
|
||||
'description' => 'nullable|string',
|
||||
'category' => 'nullable|string|max:255',
|
||||
'subcategory' => 'nullable|string|max:255',
|
||||
'barcode' => 'nullable|string|max:255',
|
||||
'photourl' => 'nullable|array',
|
||||
'photourl.*' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$errors[] = "Row " . ($index + 1) . ": " . implode(', ', $validator->errors()->all());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reject if a global product with this name already exists.
|
||||
// Owners should pick the existing one via the fuzzy-search
|
||||
// modal instead of creating a duplicate global entry.
|
||||
$duplicate = Product::whereRaw('LOWER(TRIM(name)) = ?', [strtolower(trim((string) $productData['name']))])
|
||||
->first();
|
||||
if ($duplicate) {
|
||||
$errors[] = "Row " . ($index + 1) . ": '{$productData['name']}' already exists globally. Use 'Pick existing' to import it instead.";
|
||||
continue;
|
||||
}
|
||||
|
||||
$product = Product::create([
|
||||
'name' => $productData['name'],
|
||||
'description' => $productData['description'] ?? '',
|
||||
'price' => $productData['price'],
|
||||
'unitname' => $productData['unitname'],
|
||||
'available' => $productData['available'],
|
||||
'barcode' => $productData['barcode'] ?? null,
|
||||
'category' => $productData['category'] ?? null,
|
||||
'subcategory' => $productData['subcategory'] ?? null,
|
||||
'photourl' => $productData['photourl'] ?? [],
|
||||
'created_by' => $user->id,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
if ($targetStore) {
|
||||
$targetStore->products()->attach($product->id, [
|
||||
'available' => (int) $productData['available'],
|
||||
'price' => (float) $productData['price'],
|
||||
'description' => $productData['description'] ?? '',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
$results[] = $product->hashkey;
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
DB::rollBack();
|
||||
return Response::json(['success' => false, 'message' => 'Batch creation failed', 'errors' => $errors], 422);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
return Response::json(['success' => true, 'count' => count($results), 'data' => $results]);
|
||||
} catch (\Throwable $th) {
|
||||
DB::rollBack();
|
||||
return ResponseHelper::returnError($th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve the batch products Excel template as a file download.
|
||||
* Available for all authenticated users with batch module access.
|
||||
*/
|
||||
public function downloadProductTemplate()
|
||||
{
|
||||
$templatePath = BASE_PATH . '/resources/templates/batch-products-template.xlsx';
|
||||
|
||||
if (!file_exists($templatePath)) {
|
||||
return Response::json(['success' => false, 'message' => 'Template file not found.'], 404);
|
||||
}
|
||||
|
||||
$fileContent = file_get_contents($templatePath);
|
||||
$filename = 'bukidbounty-batch-products-template.xlsx';
|
||||
|
||||
return Response::make($fileContent, 200, [
|
||||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
'Content-Length' => strlen($fileContent),
|
||||
'Cache-Control' => 'no-cache, no-store, must-revalidate',
|
||||
'Pragma' => 'no-cache',
|
||||
'Expires' => '0',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch create stores.
|
||||
* Available for Ultimate, Super Operator, and Operator.
|
||||
*/
|
||||
public function batchCreateStores(Request $request)
|
||||
{
|
||||
$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) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$stores = $request->input('stores', []);
|
||||
if (empty($stores)) {
|
||||
return ResponseHelper::returnError('No stores provided');
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$errors = [];
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
foreach ($stores as $index => $storeData) {
|
||||
$validator = Validator::make($storeData, [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'required|string',
|
||||
'address' => 'required|string',
|
||||
'category' => 'nullable|string|max:100',
|
||||
'subcategory' => 'nullable|string|max:100',
|
||||
'owner_hash' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$errors[] = "Row " . ($index + 1) . ": " . implode(', ', $validator->errors()->all());
|
||||
continue;
|
||||
}
|
||||
|
||||
$ownerId = null;
|
||||
if (!empty($storeData['owner_hash'])) {
|
||||
$ownerId = UserController::findUserIdByHash($storeData['owner_hash']);
|
||||
}
|
||||
|
||||
$storeCode = StoreController::generateStoreCode($storeData['category'] ?? 'General');
|
||||
|
||||
$store = Store::create([
|
||||
'storecode' => $storeCode,
|
||||
'name' => $storeData['name'],
|
||||
'description' => $storeData['description'],
|
||||
'address' => $storeData['address'],
|
||||
'category' => $storeData['category'] ?? 'General',
|
||||
'subcategory' => $storeData['subcategory'] ?? '',
|
||||
'owner_id' => $ownerId,
|
||||
'created_by' => $user->id,
|
||||
'is_active' => true,
|
||||
'status' => 'active',
|
||||
]);
|
||||
|
||||
$results[] = $store->hashkey;
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
DB::rollBack();
|
||||
return Response::json(['success' => false, 'message' => 'Batch creation failed', 'errors' => $errors], 422);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
return Response::json(['success' => true, 'count' => count($results), 'data' => $results]);
|
||||
} catch (\Throwable $th) {
|
||||
DB::rollBack();
|
||||
return ResponseHelper::returnError($th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch create users.
|
||||
* Available for Ultimate, Super Operator, and Operator.
|
||||
*/
|
||||
public function batchCreateUsers(Request $request)
|
||||
{
|
||||
$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) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$usersData = $request->input('users', []);
|
||||
if (empty($usersData)) {
|
||||
return ResponseHelper::returnError('No users provided');
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$errors = [];
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
foreach ($usersData as $index => $data) {
|
||||
$validator = Validator::make($data, [
|
||||
'username' => 'required|string|max:255|unique:users,username',
|
||||
'name' => 'required|string|max:255',
|
||||
'mobile_number' => ['required', 'string', 'max:20', 'unique:users,mobile_number', 'regex:/^(09|\+639)\d{9}$/'],
|
||||
'password' => 'required|string|min:6',
|
||||
'type' => 'required|string',
|
||||
'parent_hash' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$errors[] = "Row " . ($index + 1) . ": " . implode(', ', $validator->errors()->all());
|
||||
continue;
|
||||
}
|
||||
|
||||
$usertypeEnum = UserTypes::tryFrom($data['type']);
|
||||
if (!$usertypeEnum) {
|
||||
$errors[] = "Row " . ($index + 1) . ": Invalid User Type";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if creator is allowed to create this user type
|
||||
if ($acctType !== UserTypes::ULTIMATE) {
|
||||
$allowedTypes = UserTypeService::getAllowedUserTypes($acctType);
|
||||
if (!in_array($usertypeEnum, $allowedTypes)) {
|
||||
$errors[] = "Row " . ($index + 1) . ": You are not allowed to create user type '{$data['type']}'";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$parentId = $user->id; // Default to creator
|
||||
if (!empty($data['parent_hash'])) {
|
||||
$parent = User::where('hashkey', $data['parent_hash'])->first();
|
||||
if ($parent) {
|
||||
$parentId = $parent->id;
|
||||
}
|
||||
}
|
||||
|
||||
$newUser = User::create([
|
||||
'username' => $data['username'],
|
||||
'name' => $data['name'],
|
||||
'mobile_number' => $data['mobile_number'],
|
||||
'password' => Hash::make($data['password']),
|
||||
'acct_type' => $data['type'],
|
||||
'parentuid' => $parentId,
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$results[] = $newUser->hashkey;
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
DB::rollBack();
|
||||
return Response::json(['success' => false, 'message' => 'Batch creation failed', 'errors' => $errors], 422);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
return Response::json(['success' => true, 'count' => count($results), 'data' => $results]);
|
||||
} catch (\Throwable $th) {
|
||||
DB::rollBack();
|
||||
return ResponseHelper::returnError($th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch create cooperatives.
|
||||
* Available for Ultimate and Super Operator.
|
||||
*/
|
||||
public function batchCreateCooperatives(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (!$user) return ResponseHelper::returnUnauthorized();
|
||||
|
||||
$acctType = $user->acct_type instanceof UserTypes ? $user->acct_type : UserTypes::tryFrom($user->acct_type);
|
||||
$isAllowed = in_array($acctType, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR]);
|
||||
|
||||
if (!$isAllowed) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$cooperatives = $request->input('cooperatives', []);
|
||||
if (empty($cooperatives)) {
|
||||
return ResponseHelper::returnError('No cooperatives provided');
|
||||
}
|
||||
|
||||
$results = [];
|
||||
$errors = [];
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
foreach ($cooperatives as $index => $data) {
|
||||
$validator = Validator::make($data, [
|
||||
'name' => 'required|string|max:255',
|
||||
'address' => 'nullable|string',
|
||||
'registration_number' => 'nullable|string|max:255',
|
||||
'cin' => 'nullable|string|max:255',
|
||||
'tin' => 'nullable|string|max:255',
|
||||
'cooperative_type' => 'nullable|string|max:100',
|
||||
'cooperative_category' => 'nullable|string|max:100',
|
||||
'registration_date' => 'nullable|date',
|
||||
'contact_person' => 'nullable|string|max:255',
|
||||
'contact_number' => 'nullable|string|max:50',
|
||||
'contact_email' => 'nullable|email|max:255',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$errors[] = "Row " . ($index + 1) . ": " . implode(', ', $validator->errors()->all());
|
||||
continue;
|
||||
}
|
||||
|
||||
$cooperative = new Organization([
|
||||
'hashkey' => Str::random(64),
|
||||
'name' => trim($data['name']),
|
||||
'type' => 'COOPERATIVE',
|
||||
'address' => trim($data['address'] ?? ''),
|
||||
'registration_number' => trim($data['registration_number'] ?? ''),
|
||||
'cin' => trim($data['cin'] ?? ''),
|
||||
'tin' => trim($data['tin'] ?? ''),
|
||||
'cooperative_type' => trim($data['cooperative_type'] ?? ''),
|
||||
'cooperative_category' => trim($data['cooperative_category'] ?? ''),
|
||||
'registration_date' => $data['registration_date'] ?? null,
|
||||
'contact_person' => trim($data['contact_person'] ?? ''),
|
||||
'contact_number' => trim($data['contact_number'] ?? ''),
|
||||
'contact_email' => trim($data['contact_email'] ?? ''),
|
||||
'is_active' => true,
|
||||
'created_by' => $user->id,
|
||||
]);
|
||||
|
||||
if (!$cooperative->save()) {
|
||||
$errors[] = "Row " . ($index + 1) . ": Failed to save cooperative";
|
||||
continue;
|
||||
}
|
||||
|
||||
$results[] = $cooperative->hashkey;
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
DB::rollBack();
|
||||
return Response::json(['success' => false, 'message' => 'Batch creation failed', 'errors' => $errors], 422);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
return Response::json(['success' => true, 'count' => count($results), 'data' => $results]);
|
||||
} catch (\Throwable $th) {
|
||||
DB::rollBack();
|
||||
return ResponseHelper::returnError($th->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch add cooperative members with automatic user account creation.
|
||||
* Each row creates a new User and a corresponding CooperativeMember in one transaction.
|
||||
*/
|
||||
public function batchCreateCooperativeMembers(Request $request)
|
||||
{
|
||||
$user = Auth::user();
|
||||
if (!$user) return ResponseHelper::returnUnauthorized();
|
||||
|
||||
if (!UserPermissions::isActionPermitted($user->acct_type, UserActions::ManageOrganizations)) {
|
||||
return ResponseHelper::returnUnauthorized();
|
||||
}
|
||||
|
||||
$cooperativeHash = $request->input('cooperative_hash');
|
||||
$members = $request->input('members', []);
|
||||
|
||||
if (!$cooperativeHash) {
|
||||
return ResponseHelper::returnError('Cooperative hash is required');
|
||||
}
|
||||
if (empty($members)) {
|
||||
return ResponseHelper::returnError('No members provided');
|
||||
}
|
||||
|
||||
$cooperative = Organization::where('hashkey', $cooperativeHash)
|
||||
->where('type', 'COOPERATIVE')
|
||||
->first();
|
||||
|
||||
if (!$cooperative) {
|
||||
return ResponseHelper::returnError('Cooperative not found', 404);
|
||||
}
|
||||
|
||||
$parentUser = User::where('id', $cooperative->created_by)->first() ?? $user;
|
||||
|
||||
$results = [];
|
||||
$errors = [];
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
foreach ($members as $index => $data) {
|
||||
$validator = Validator::make($data, [
|
||||
'username' => 'required|string|max:255|unique:users,username',
|
||||
'name' => 'required|string|max:255',
|
||||
'mobile_number' => ['required', 'string', 'max:20', 'unique:users,mobile_number', 'regex:/^(09|\+639)\d{9}$/'],
|
||||
'password' => 'required|string|min:6',
|
||||
'role' => 'nullable|string|max:50',
|
||||
'membership_type' => 'nullable|string|max:50',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$errors[] = 'Row ' . ($index + 1) . ': ' . implode(', ', $validator->errors()->all());
|
||||
continue;
|
||||
}
|
||||
|
||||
$newUser = new User();
|
||||
$newUser->username = $data['username'];
|
||||
$newUser->name = $data['name'];
|
||||
$newUser->mobile_number = $data['mobile_number'];
|
||||
$newUser->password = Hash::make($data['password']);
|
||||
$newUser->parentuid = $parentUser->id;
|
||||
$newUser->acct_type = 'user';
|
||||
$newUser->active = true;
|
||||
$newUser->save();
|
||||
|
||||
$member = new CooperativeMember([
|
||||
'hashkey' => Str::random(64),
|
||||
'organization_id' => $cooperative->id,
|
||||
'user_id' => $newUser->id,
|
||||
'role' => $data['role'] ?? 'MEMBER',
|
||||
'membership_type' => $data['membership_type'] ?? null,
|
||||
'joined_at' => now(),
|
||||
'is_active' => true,
|
||||
'created_by' => $user->id,
|
||||
]);
|
||||
|
||||
if (!$member->save()) {
|
||||
$errors[] = 'Row ' . ($index + 1) . ': Failed to save membership';
|
||||
continue;
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'user_hashkey' => $newUser->hashkey,
|
||||
'member_hashkey' => $member->hashkey,
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
DB::rollBack();
|
||||
return Response::json(['success' => false, 'message' => 'Batch creation failed', 'errors' => $errors], 422);
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
return Response::json(['success' => true, 'count' => count($results), 'data' => $results]);
|
||||
} catch (\Throwable $th) {
|
||||
DB::rollBack();
|
||||
return ResponseHelper::returnError($th->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user