initial: bootstrap from BukidBountyApp base

This commit is contained in:
Jonathan Sykes
2026-06-06 18:43:00 +08:00
commit eb4a5731fb
5674 changed files with 160857 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
abstract class AbstractController
{
}

View File

@@ -0,0 +1,751 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Accounting;
use App\Http\Controllers\AbstractController;
use App\Models\Accounting\Account;
use App\Models\Accounting\AccountTransaction;
use App\Models\Market\Store;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Support\ModuleHelper;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\DB;
use Hyperf\Stringable\Str;
class AccountingController extends AbstractController
{
// ─── helpers ────────────────────────────────────────────────────────────
private function resolveType(): UserTypes
{
$user = Auth::user();
return $user->acct_type instanceof UserTypes
? $user->acct_type
: (UserTypes::tryFrom($user->acct_type) ?? UserTypes::PUBLIC);
}
private function isBig3(UserTypes $t): bool
{
return in_array($t, [UserTypes::ULTIMATE, UserTypes::SUPER_OPERATOR, UserTypes::OPERATOR], true);
}
private function isStoreLevel(UserTypes $t): bool
{
return in_array($t, [UserTypes::STORE_OWNER, UserTypes::STORE_MANAGER], true);
}
/** IDs of stores the current user owns or manages (including descendants). */
private function userStoreIds(): array
{
$user = Auth::user();
$allowedUserIds = array_merge([$user->id], $user->getAllDescendants()->pluck('id')->toArray());
return 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);
});
})->pluck('id')->toArray();
}
/** Apply store_id scope to a query based on the caller's role. */
private function scopeAccounts($query, array $storeIds, bool $isBig3)
{
if ($isBig3) {
return $query->whereNull('store_id');
}
return $query->whereIn('store_id', $storeIds);
}
/** Check store-level access. Returns false if the accounting_store module is disabled. */
private function guardStore(): bool
{
return ModuleHelper::isEnabled('accounting_store');
}
// ─── public endpoints ────────────────────────────────────────────────────
/**
* Full Chart of Accounts tree.
*/
public function getAccountsTree(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$base = Account::whereNull('parent_id')->where('is_active', true);
$base = $this->scopeAccounts($base, $storeIds, $isBig3);
$accounts = $base
->with(['children' => function ($q) use ($storeIds, $isBig3) {
$q = $q->where('is_active', true);
$q = $this->scopeAccounts($q, $storeIds, $isBig3);
$q->with(['children' => function ($q2) use ($storeIds, $isBig3) {
$q2 = $q2->where('is_active', true);
$q2 = $this->scopeAccounts($q2, $storeIds, $isBig3);
}]);
}])
->orderBy('id')
->get();
return response()->json(['success' => true, 'data' => $accounts]);
}
/**
* Leaf accounts for the Daily Entry form.
*/
public function getLeafAccounts(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$base = Account::where('is_active', true)->whereDoesntHave('children');
$base = $this->scopeAccounts($base, $storeIds, $isBig3);
$leafAccounts = $base
->with('parent.parent')
->orderBy('id')
->get();
return response()->json(['success' => true, 'data' => $leafAccounts]);
}
/**
* Daily transactions for a given date.
*/
public function getDailyTransactions(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$date = $request->input('date', date('Y-m-d'));
$accountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id');
$transactions = AccountTransaction::with(['account'])
->whereIn('account_id', $accountIds)
->whereDate('transaction_date', $date)
->get();
$mapped = [];
foreach ($transactions as $txn) {
$mapped[$txn->account_id] = [
'id' => $txn->id,
'amount' => $txn->amount,
'notes' => $txn->notes,
];
}
return response()->json(['success' => true, 'date' => $date, 'data' => $mapped]);
}
/**
* Bulk save daily transactions.
*/
public function saveDailyTransactions(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$date = $request->input('date');
$entries = $request->input('entries', []);
$userId = Auth::id();
// Allowed account IDs for this user (prevent writing to other stores' accounts)
$allowedAccountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id')->flip()->all();
DB::beginTransaction();
try {
foreach ($entries as $accountId => $entryData) {
if (!array_key_exists($accountId, $allowedAccountIds)) {
continue; // skip accounts not owned by this user
}
$amount = (float)($entryData['amount'] ?? 0);
$account = Account::find($accountId);
if (!$account) {
continue;
}
$flow = $account->default_flow
?: ((strtoupper((string) $account->type) === 'REVENUE' || strtoupper((string) $account->type) === 'LIABILITY') ? 'INCOME' : 'EXPENSE');
$txn = AccountTransaction::where('account_id', $accountId)
->whereDate('transaction_date', $date)
->first();
if ($amount == 0) {
if ($txn) {
$txn->delete();
}
continue;
}
if ($txn) {
$txn->amount = $amount;
$txn->notes = $entryData['notes'] ?? null;
$txn->updated_by = $userId;
$txn->save();
} else {
AccountTransaction::create([
'hashkey' => Str::random(32),
'account_id' => $accountId,
'amount' => $amount,
'flow' => $flow,
'transaction_date' => $date,
'notes' => $entryData['notes'] ?? null,
'created_by' => $userId,
'updated_by' => $userId,
]);
}
}
DB::commit();
return response()->json(['success' => true, 'message' => 'Daily transactions saved.']);
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['success' => false, 'message' => 'Error saving: ' . $e->getMessage()]);
}
}
/**
* Transaction list for CRUD tab.
*/
public function listTransactions(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$accountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id');
$query = AccountTransaction::with(['account', 'creator', 'updater'])
->whereIn('account_id', $accountIds);
if ($request->filled('account_id')) {
$query->where('account_id', $request->account_id);
}
if ($request->filled('date_from')) {
$query->whereDate('transaction_date', '>=', $request->date_from);
}
if ($request->filled('date_to')) {
$query->whereDate('transaction_date', '<=', $request->date_to);
}
$transactions = $query->orderBy('transaction_date', 'desc')->paginate(50);
return response()->json(['success' => true, 'data' => $transactions]);
}
/**
* Delete single transaction.
*/
public function deleteTransaction(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$allowedAccountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id')->all();
$txn = AccountTransaction::find($request->id);
if ($txn && in_array($txn->account_id, $allowedAccountIds)) {
$txn->delete();
}
return response()->json(['success' => true]);
}
/**
* Monthly matrix report (Rows = Days, Columns = Accounts).
*/
public function getMonthlyReport(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$year = $request->input('year', date('Y'));
$month = $request->input('month', date('m'));
$accountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id')->all();
$transactions = AccountTransaction::whereYear('transaction_date', $year)
->whereMonth('transaction_date', $month)
->whereIn('account_id', $accountIds)
->get();
$daysInMonth = cal_days_in_month(CAL_GREGORIAN, (int)$month, (int)$year);
$matrix = [];
for ($i = 1; $i <= $daysInMonth; $i++) {
$matrix[$i] = [];
}
foreach ($transactions as $txn) {
$day = (int)date('d', strtotime($txn->transaction_date));
$matrix[$day][$txn->account_id] = ($matrix[$day][$txn->account_id] ?? 0) + $txn->amount;
}
$columnTotals = [];
foreach ($transactions as $txn) {
$columnTotals[$txn->account_id] = ($columnTotals[$txn->account_id] ?? 0) + $txn->amount;
}
return response()->json([
'success' => true,
'year' => $year,
'month' => $month,
'days_in_month' => $daysInMonth,
'matrix' => $matrix,
'column_totals' => $columnTotals,
]);
}
/**
* Create a new account node. For store-level users the account is
* automatically scoped to their store (first owned store).
*/
public function createAccount(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$name = trim((string) $request->input('name', ''));
$accountType = strtoupper(trim((string) $request->input('type', '')));
$defaultFlow = strtoupper(trim((string) $request->input('default_flow', '')));
$parentId = $request->input('parent_id') ?: null;
$description = $request->input('description') ?: null;
if ($name === '' || $accountType === '') {
return response()->json(['success' => false, 'message' => 'Name and type are required.'], 422);
}
if (!in_array($defaultFlow, ['INCOME', 'EXPENSE'], true)) {
$defaultFlow = in_array($accountType, ['REVENUE', 'LIABILITY']) ? 'INCOME' : 'EXPENSE';
}
if ($parentId !== null && !Account::where('id', $parentId)->exists()) {
return response()->json(['success' => false, 'message' => 'Parent account not found.'], 422);
}
// Determine store_id: null for Big3 (global), first store for store-level users
$storeId = null;
if (!$isBig3) {
$storeIds = $this->userStoreIds();
if (empty($storeIds)) {
return response()->json(['success' => false, 'message' => 'No store found for your account. Create a store first.'], 422);
}
// If a parent is supplied, inherit its store_id; otherwise use the request-supplied store_id or the user's first store
if ($parentId) {
$parent = Account::find($parentId);
$storeId = $parent?->store_id ?? $storeIds[0];
} else {
$requestedStoreId = $request->input('store_id');
$storeId = ($requestedStoreId && in_array($requestedStoreId, $storeIds))
? (int)$requestedStoreId
: $storeIds[0];
}
}
try {
$account = Account::create([
'hashkey' => Str::random(40),
'parent_id' => $parentId,
'store_id' => $storeId,
'type' => $accountType,
'default_flow' => $defaultFlow,
'name' => $name,
'description' => $description,
'is_active' => true,
'created_by' => Auth::id(),
'updated_by' => Auth::id(),
]);
} catch (\Throwable $e) {
\Hypervel\Support\Facades\Log::error('createAccount failed', [
'message' => $e->getMessage(),
'name' => $name,
'type' => $accountType,
'parent_id' => $parentId,
'store_id' => $storeId,
'user_id' => Auth::id(),
]);
return response()->json([
'success' => false,
'message' => 'Could not create account: ' . $e->getMessage(),
], 422);
}
return response()->json(['success' => true, 'data' => $account]);
}
/**
* Update editable fields (name, type, default_flow, description).
*/
public function updateAccount(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$account = Account::find($request->input('id'));
if (!$account) {
return response()->json(['success' => false, 'message' => 'Account not found.'], 404);
}
// Store-level users may only edit their own store's accounts
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3) {
if (!$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $this->userStoreIds();
if (!in_array($account->store_id, $storeIds)) {
return ResponseHelper::returnUnauthorized();
}
}
if ($request->filled('name')) {
$account->name = trim((string) $request->input('name'));
}
if ($request->filled('type')) {
$account->type = strtoupper(trim((string) $request->input('type')));
}
if ($request->filled('default_flow')) {
$flow = strtoupper(trim((string) $request->input('default_flow')));
if (!in_array($flow, ['INCOME', 'EXPENSE'], true)) {
return response()->json(['success' => false, 'message' => 'default_flow must be INCOME or EXPENSE.'], 422);
}
$account->default_flow = $flow;
}
if ($request->has('description')) {
$account->description = $request->input('description') ?: null;
}
$account->updated_by = Auth::id();
$account->save();
return response()->json(['success' => true, 'data' => $account]);
}
/**
* Archive (soft-disable) an account.
*/
public function archiveAccount(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$account = Account::find($request->input('id'));
if (!$account) {
return response()->json(['success' => false, 'message' => 'Account not found.'], 404);
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3) {
if (!$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $this->userStoreIds();
if (!in_array($account->store_id, $storeIds)) {
return ResponseHelper::returnUnauthorized();
}
}
if (Account::where('parent_id', $account->id)->where('is_active', true)->exists()) {
return response()->json(['success' => false, 'message' => 'Archive child accounts first.'], 422);
}
if (AccountTransaction::where('account_id', $account->id)->exists()) {
return response()->json(['success' => false, 'message' => 'Cannot archive: account has transactions. Move or delete them first.'], 422);
}
$account->is_active = false;
$account->updated_by = Auth::id();
$account->save();
return response()->json(['success' => true]);
}
/**
* Move an account under a new parent (or to root). Refuses cycles.
*/
public function moveAccount(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$account = Account::find($request->input('id'));
if (!$account) {
return response()->json(['success' => false, 'message' => 'Account not found.'], 404);
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3) {
if (!$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $this->userStoreIds();
if (!in_array($account->store_id, $storeIds)) {
return ResponseHelper::returnUnauthorized();
}
}
$newParentId = $request->input('parent_id');
$newParentId = ($newParentId === null || $newParentId === '' || $newParentId == 0) ? null : (int)$newParentId;
if ($newParentId === (int)$account->id) {
return response()->json(['success' => false, 'message' => 'An account cannot be its own parent.'], 422);
}
if ($newParentId !== null) {
$parent = Account::find($newParentId);
if (!$parent) {
return response()->json(['success' => false, 'message' => 'Target parent not found.'], 422);
}
$cursor = $parent;
$hops = 0;
while ($cursor && $hops < 1000) {
if ((int)$cursor->id === (int)$account->id) {
return response()->json(['success' => false, 'message' => 'Cannot move an account under one of its own descendants.'], 422);
}
$cursor = $cursor->parent_id ? Account::find($cursor->parent_id) : null;
$hops++;
}
}
$account->parent_id = $newParentId;
$account->updated_by = Auth::id();
$account->save();
return response()->json(['success' => true]);
}
/**
* Re-activate an archived account.
*/
public function restoreAccount(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$account = Account::find($request->input('id'));
if (!$account) {
return response()->json(['success' => false, 'message' => 'Account not found.'], 404);
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3) {
if (!$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $this->userStoreIds();
if (!in_array($account->store_id, $storeIds)) {
return ResponseHelper::returnUnauthorized();
}
}
$account->is_active = true;
$account->updated_by = Auth::id();
$account->save();
return response()->json(['success' => true]);
}
/**
* Theme metadata + drift summary for Manage Accounts.
*/
public function getThemeInfo(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$current = \App\Support\AccountingTheme::current();
$definition = \App\Support\AccountingTheme::definition($current);
return response()->json([
'success' => true,
'current' => $current,
'definition' => [
'label' => $definition['label'] ?? $current,
'description' => $definition['description'] ?? null,
'version' => $definition['version'] ?? 1,
],
'options' => \App\Support\AccountingTheme::options(),
'counts' => [
'theme_tagged' => Account::where('theme_key', $current)->count(),
'user_added' => Account::whereNull('theme_key')->count(),
'archived' => Account::where('is_active', false)->count(),
],
]);
}
/**
* Read-only drift report.
*/
public function getThemeDrift(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
return response()->json([
'success' => true,
'data' => \App\Support\AccountingTheme::drift(),
]);
}
/**
* Switch the active accounting theme.
*/
public function setTheme(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
$key = trim((string) $request->input('key', ''));
if ($key === '' || !\array_key_exists($key, \App\Support\AccountingTheme::all())) {
return response()->json(['success' => false, 'message' => 'Unknown theme.'], 422);
}
\App\Models\SystemSetting::setValue('accounting_theme', $key, 'accounting', 'string');
return response()->json(['success' => true, 'key' => $key]);
}
/**
* Idempotently re-apply the active theme (additive only).
*/
public function applyTheme(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageAccounting)) {
return ResponseHelper::returnUnauthorized();
}
try {
$stats = \App\Support\AccountingTheme::apply();
} catch (\Throwable $e) {
return response()->json([
'success' => false,
'message' => 'Failed to apply theme: ' . $e->getMessage(),
], 422);
}
return response()->json([
'success' => true,
'message' => sprintf(
'Theme "%s" applied: %d created, %d updated, %d unchanged.',
$stats['theme_key'],
$stats['created'],
$stats['stamped'],
$stats['skipped']
),
'data' => $stats,
]);
}
/**
* Recent transactions for the Reports page.
*/
public function listReports(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAccountingReports)) {
return ResponseHelper::returnUnauthorized();
}
$type = $this->resolveType();
$isBig3 = $this->isBig3($type);
if (!$isBig3 && !$this->guardStore()) {
return ResponseHelper::returnUnauthorized();
}
$storeIds = $isBig3 ? [] : $this->userStoreIds();
$accountIds = $this->scopeAccounts(Account::query(), $storeIds, $isBig3)->pluck('id');
$transactions = AccountTransaction::with(['account', 'creator'])
->whereIn('account_id', $accountIds)
->orderBy('transaction_date', 'desc')
->limit(100)
->get();
return response()->json(['success' => true, 'transactions' => $transactions]);
}
}

View File

@@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\PersonalAccessToken;
use App\Models\User;
use App\Support\TokenAbilities;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
class ApiTokenController
{
public function catalog()
{
return response()->json([
'success' => true,
'data' => [
'abilities' => TokenAbilities::catalog(),
'wildcard' => TokenAbilities::WILDCARD,
],
]);
}
public function index(Request $request)
{
$tokens = PersonalAccessToken::query()
->where('tokenable_type', User::class)
->orderByDesc('id')
->get()
->map(fn ($t) => $this->present($t));
return response()->json(['success' => true, 'data' => $tokens]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:120',
'description' => 'nullable|string|max:500',
'abilities' => 'required|array|min:1',
'abilities.*' => 'string',
'allowed_ips' => 'nullable|array',
'allowed_ips.*' => 'string',
'expires_at' => 'nullable|date',
'tokenable_user_id' => 'nullable|integer',
]);
foreach ($validated['abilities'] as $ability) {
if (! TokenAbilities::exists($ability)) {
return ResponseHelper::returnError("Unknown ability: {$ability}", 422);
}
}
$owner = Auth::user();
$tokenable = $owner;
if (! empty($validated['tokenable_user_id'])) {
$tokenable = User::query()->find($validated['tokenable_user_id']);
if (! $tokenable) {
return ResponseHelper::returnError('Target user not found.', 404);
}
}
$result = $tokenable->createToken(
name: $validated['name'],
abilities: $validated['abilities'],
allowedIps: $validated['allowed_ips'] ?? null,
expiresAt: !empty($validated['expires_at']) ? new \DateTimeImmutable($validated['expires_at']) : null,
description: $validated['description'] ?? null,
createdBy: $owner->id,
);
return response()->json([
'success' => true,
'data' => [
'token' => $this->present($result['token']),
'plain_text_token' => $result['plainTextToken'],
],
'message' => 'Token created. Copy it now — it will not be shown again.',
]);
}
public function revoke(Request $request, int $id)
{
$token = PersonalAccessToken::query()->find($id);
if (! $token) {
return ResponseHelper::returnError('Token not found.', 404);
}
if ($token->revoked_at !== null) {
return ResponseHelper::returnError('Token already revoked.', 422);
}
$token->forceFill([
'revoked_at' => now(),
'revoked_by' => Auth::id(),
])->save();
return response()->json(['success' => true, 'data' => $this->present($token)]);
}
public function destroy(Request $request, int $id)
{
$token = PersonalAccessToken::query()->find($id);
if (! $token) {
return ResponseHelper::returnError('Token not found.', 404);
}
$token->delete();
return response()->json(['success' => true]);
}
private function present(PersonalAccessToken $t): array
{
return [
'id' => $t->id,
'name' => $t->name,
'description' => $t->description,
'tokenable_id' => $t->tokenable_id,
'tokenable_type' => $t->tokenable_type,
'abilities' => $t->abilities ?? [],
'allowed_ips' => $t->allowed_ips ?? [],
'expires_at' => $t->expires_at,
'last_used_at' => $t->last_used_at,
'last_used_ip' => $t->last_used_ip,
'revoked_at' => $t->revoked_at,
'created_by' => $t->created_by,
'created_at' => $t->created_at,
'is_active' => $t->isActive(),
];
}
}

View File

@@ -0,0 +1,268 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\LandingPage;
use App\Enums\UserTypes;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use Exception;
class LandingPageController
{
/**
* Allowed roles for landing page management.
*/
private static array $allowedTypes = [
'ult',
'super operator',
'coordinator',
];
/**
* Check if the current user is authorized.
*/
private function isAuthorized(): bool
{
$user = Auth::user();
if (!$user) return false;
$type = $user->acct_type;
if ($type instanceof UserTypes) {
$type = $type->value;
}
return in_array($type, self::$allowedTypes);
}
/**
* List all landing pages.
*/
public function index(Request $request)
{
if (!$this->isAuthorized()) {
return ResponseHelper::returnUnauthorized();
}
$pages = LandingPage::orderByDesc('is_active')
->orderByDesc('updated_at')
->get();
return response()->json([
'success' => true,
'data' => $pages,
]);
}
/**
* Get a single landing page by hashkey.
*/
public function show(Request $request)
{
if (!$this->isAuthorized()) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('hashkey');
if (!$hashkey) {
return response()->json(['success' => false, 'message' => 'Hashkey is required'], 400);
}
$page = LandingPage::where('hashkey', $hashkey)->first();
if (!$page) {
return response()->json(['success' => false, 'message' => 'Landing page not found'], 404);
}
return response()->json([
'success' => true,
'data' => $page,
]);
}
/**
* Create or update a landing page.
*/
public function store(Request $request)
{
if (!$this->isAuthorized()) {
return ResponseHelper::returnUnauthorized();
}
$validated = $request->validate([
'title' => 'required|string|max:255',
'html_content' => 'required|string',
'description' => 'nullable|string|max:1000',
'hashkey' => 'nullable|string', // If provided, it's an update
]);
try {
$hashkey = $validated['hashkey'] ?? null;
if ($hashkey) {
// Update existing
$page = LandingPage::where('hashkey', $hashkey)->first();
if (!$page) {
return response()->json(['success' => false, 'message' => 'Landing page not found'], 404);
}
$page->title = $validated['title'];
$page->html_content = $validated['html_content'];
$page->description = $validated['description'] ?? $page->description;
$page->save();
return response()->json([
'success' => true,
'message' => 'Landing page updated successfully',
'data' => $page,
]);
} else {
// Create new
$page = LandingPage::create([
'title' => $validated['title'],
'html_content' => $validated['html_content'],
'description' => $validated['description'] ?? null,
'is_active' => false,
]);
return response()->json([
'success' => true,
'message' => 'Landing page created successfully',
'data' => $page,
]);
}
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to save landing page: ' . $e->getMessage(),
], 500);
}
}
/**
* Set a landing page as active.
*/
public function setActive(Request $request)
{
if (!$this->isAuthorized()) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('hashkey');
if (!$hashkey) {
return response()->json(['success' => false, 'message' => 'Hashkey is required'], 400);
}
try {
$page = LandingPage::where('hashkey', $hashkey)->first();
if (!$page) {
return response()->json(['success' => false, 'message' => 'Landing page not found'], 404);
}
$page->setAsActive();
return response()->json([
'success' => true,
'message' => 'Landing page activated successfully',
'data' => $page,
]);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to activate landing page: ' . $e->getMessage(),
], 500);
}
}
/**
* Deactivate all landing pages (show default homepage for guests).
*/
public function deactivateAll(Request $request)
{
if (!$this->isAuthorized()) {
return ResponseHelper::returnUnauthorized();
}
try {
LandingPage::where('is_active', true)->update(['is_active' => false]);
return response()->json([
'success' => true,
'message' => 'All landing pages deactivated. Default homepage will be shown.',
]);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to deactivate: ' . $e->getMessage(),
], 500);
}
}
/**
* Delete a landing page.
*/
public function destroy(Request $request)
{
if (!$this->isAuthorized()) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('hashkey');
if (!$hashkey) {
return response()->json(['success' => false, 'message' => 'Hashkey is required'], 400);
}
try {
$page = LandingPage::where('hashkey', $hashkey)->first();
if (!$page) {
return response()->json(['success' => false, 'message' => 'Landing page not found'], 404);
}
$page->delete();
return response()->json([
'success' => true,
'message' => 'Landing page deleted successfully',
]);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to delete landing page: ' . $e->getMessage(),
], 500);
}
}
/**
* Public endpoint: Get the currently active landing page content.
* No authentication required - used to show landing page to guests.
*/
public function getActiveLandingPage()
{
try {
$page = LandingPage::getActive();
} catch (\Throwable $e) {
return Response::json(['success' => false, 'data' => null, 'has_landing_page' => false]);
}
if (!$page) {
return Response::json([
'success' => true,
'data' => null,
'has_landing_page' => false,
]);
}
return Response::json([
'success' => true,
'data' => [
'title' => $page->title,
'html_content' => $page->html_content,
'description' => $page->description,
],
'has_landing_page' => true,
]);
}
}

View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\PosAccessKey;
use App\Models\Market\Store;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hyperf\Stringable\Str;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
class PosAccessKeyController
{
/**
* List POS access keys. Auto-expires any past-due keys first.
* - Ultimate/super operator/operator: see all keys
* - Store owner/manager: see keys for stores they own or manage
*/
public function index(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewPosAccessKeys)) {
return ResponseHelper::returnUnauthorized();
}
// Auto-deactivate any expired keys
PosAccessKey::autoExpire();
$user = Auth::user();
$acctType = $user->acct_type->value ?? $user->acct_type ?? '';
// Ultimate, Super Operator, and Operator see everything
if (in_array($acctType, ['ult', 'super operator', 'operator'])) {
$query = PosAccessKey::with(['store:id,name,hashkey,owner_id', 'store.owner:id,name,nickname,hashkey'])
->orderBy('id', 'desc');
} else {
// Non-ultimate users see their own and their descendants' stores' keys
$descendants = $user->getAllDescendants();
$descendantIds = $descendants->pluck('id')->push($user->id)->toArray();
$storeIds = Store::whereIn('owner_id', $descendantIds)
->orWhereIn('manager_id', $descendantIds)
->pluck('id')
->toArray();
$query = PosAccessKey::with(['store:id,name,hashkey,owner_id', 'store.owner:id,name,nickname,hashkey'])
->whereIn('store_id', $storeIds)
->orderBy('id', 'desc');
}
$keys = $query->get();
return ResponseHelper::returnSuccessResponse($keys, 'pos_access_keys');
}
public function store(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreatePosAccessKey)) {
return ResponseHelper::returnUnauthorized();
}
$validated = $request->validate([
'name' => 'required|string|max:255',
'store_hash' => 'required|string',
'expires_at' => 'nullable|string',
]);
$store = Store::where('hashkey', $validated['store_hash'])->first();
if (!$store) {
return ResponseHelper::returnError('Store not found', 404);
}
$user = Auth::user();
$acctType = $user->acct_type->value ?? $user->acct_type ?? '';
if (!in_array($acctType, ['ult', 'super operator', 'operator'])) {
$descendants = $user->getAllDescendants();
$allowedIds = $descendants->pluck('id')->push($user->id)->toArray();
if (!in_array($store->owner_id, $allowedIds) && !in_array($store->manager_id, $allowedIds)) {
return ResponseHelper::returnError('Unauthorized to create keys for this store', 403);
}
}
$data = [
'access_key' => 'PK-' . Str::upper(Str::random(16)),
'store_id' => $store->id,
'name' => $validated['name'],
'created_by' => Auth::id(),
'status' => 'active',
];
// Set expiry if provided
if (!empty($validated['expires_at'])) {
$data['expires_at'] = $validated['expires_at'];
}
$key = PosAccessKey::create($data);
return ResponseHelper::returnSuccessResponse($key, $key->hashkey, 'POS Access Key created');
}
public function destroy(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::DeletePosAccessKey)) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('target');
$key = PosAccessKey::with('store')->where('hashkey', $hashkey)->first();
if (!$key) {
return ResponseHelper::returnError('Key not found', 404);
}
if (!$this->userOwnsKeyStore($key)) {
return ResponseHelper::returnError('Unauthorized to delete this key', 403);
}
$key->delete();
return ResponseHelper::returnSuccessResponse([], $hashkey, 'Key deleted');
}
public function toggleStatus(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::TogglePosAccessKey)) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('target');
$key = PosAccessKey::with('store')->where('hashkey', $hashkey)->first();
if (!$key) {
return ResponseHelper::returnError('Key not found', 404);
}
if (!$this->userOwnsKeyStore($key)) {
return ResponseHelper::returnError('Unauthorized to modify this key', 403);
}
$key->status = $key->status === 'active' ? 'inactive' : 'active';
$key->save();
return ResponseHelper::returnSuccessResponse($key, $hashkey, 'Status updated');
}
/**
* Ownership gate shared by destroy/toggleStatus: Big 3 always pass;
* everyone else must own or manage (directly or via descendants) the
* store the key belongs to.
*/
private function userOwnsKeyStore(PosAccessKey $key): bool
{
$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;
}
$store = $key->store;
if (!$store) {
return false;
}
$allowedIds = $user->getAllDescendants()->pluck('id')->push($user->id)->toArray();
return in_array($store->owner_id, $allowedIds) || in_array($store->manager_id, $allowedIds);
}
}

View File

@@ -0,0 +1,284 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\FilesMainController;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\SystemSetting;
use App\Enums\UserTypes;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Exception;
class SystemSettingsController
{
/**
* Get all system settings grouped by group.
*/
public function index(Request $request)
{
if (!Auth::user()->isUltimate()) {
return ResponseHelper::returnUnauthorized();
}
$keys = $request->input('keys', []);
if (!empty($keys)) {
$settings = SystemSetting::whereIn('key', (array)$keys)->get();
return response()->json([
'success' => true,
'data' => $settings
]);
}
$settings = SystemSetting::all()->groupBy('group');
return response()->json([
'success' => true,
'data' => $settings
]);
}
/**
* Update multiple settings.
*/
public function update(Request $request)
{
if (!Auth::user()->isUltimate()) {
return ResponseHelper::returnUnauthorized();
}
$validated = $request->validate([
'settings' => 'required|array',
]);
try {
foreach ($validated['settings'] as $key => $value) {
SystemSetting::setValue($key, $value);
}
SystemSetting::clearCache();
return response()->json([
'success' => true,
'message' => 'System settings updated successfully'
]);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to update settings: ' . $e->getMessage()
], 500);
}
}
/**
* Upload application logo.
*/
public function uploadLogo(Request $request)
{
if (!Auth::user()->isUltimate()) {
return ResponseHelper::returnUnauthorized();
}
if (!$request->hasFile('logo')) {
return response()->json(['success' => false, 'message' => 'No logo file provided'], 400);
}
try {
$file = $request->file('logo');
$filename = 'app_logo_' . time() . '.' . $file->getExtension();
// Extract palette from the uploaded file before it is moved by storage.
$palette = \App\Support\PaletteExtractor::extract($file->getRealPath());
// Reusing existing file storage system
$result = FilesMainController::uploadFileList(
$file,
'System Logo',
$filename,
'Application branding logo',
[],
'system',
[],
0,
'app_logo',
);
if ($result && isset($result->hashkey)) {
SystemSetting::setValue('app_logo', $result->hashkey);
if ($palette) {
SystemSetting::setValue('primary_color', $palette['primary']);
SystemSetting::setValue('accent_color', $palette['accent']);
SystemSetting::setValue('background_tint', $palette['tint']);
}
SystemSetting::clearCache();
return response()->json([
'success' => true,
'hashkey' => $result->hashkey,
'url' => FilesMainController::generateURLforFileListHash($result->hashkey),
'palette' => $palette,
'message' => 'Logo uploaded successfully'
]);
}
throw new Exception('File upload failed');
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Logo upload failed: ' . $e->getMessage()
], 500);
}
}
/**
* Get public settings data.
*/
public static function getPublicSettingsData()
{
$publicKeys = ['app_name', 'app_logo', 'app_description', 'app_tagline', 'primary_color', 'accent_color', 'background_tint', 'footer_text', 'disabled_pages', 'top_up_enabled', 'app_mode', 'main_organization', 'accounting_theme', 'default_org_type', 'group_types', 'bible_verse_text', 'bible_verse_reference'];
$settings = [];
foreach ($publicKeys as $key) {
$settings[$key] = SystemSetting::getValue($key);
}
// Add module states
$settings['module_states'] = \App\Support\ModuleHelper::getModuleStates();
// Generate URL for logo if it exists
if (!empty($settings['app_logo'])) {
$settings['app_logo_url'] = FilesMainController::generateURLforFileListHash($settings['app_logo']);
} else {
$settings['app_logo_url'] = cdn_asset('vendor/assets/icons/192x192.png'); // Fallback to PWA icon on the CDN
}
// Resolve main organization hashkey to a usable data object
$settings['main_organization_data'] = null;
if (!empty($settings['main_organization'])) {
$org = \App\Models\Market\Organization::where('hashkey', $settings['main_organization'])->first();
if ($org) {
$settings['main_organization_data'] = [
'hashkey' => $org->hashkey,
'name' => $org->name,
'type' => $org->type,
];
}
}
return $settings;
}
/**
* List organizations available for selection as the main cooperative/organization.
*/
public function listOrganizations()
{
if (!Auth::user()->isUltimate()) {
return ResponseHelper::returnUnauthorized();
}
$orgs = \App\Models\Market\Organization::where('is_active', true)
->orderBy('type')
->orderBy('name')
->get(['hashkey', 'name', 'type']);
return response()->json([
'success' => true,
'data' => $orgs,
]);
}
/**
* Get all modules with their effective state, env/config default, and
* any DB-stored override. Used by the Ultimate Console module manager.
*/
public function getModules()
{
if (!Auth::user()->isUltimate()) {
return ResponseHelper::returnUnauthorized();
}
return response()->json([
'success' => true,
'data' => [
'system_enabled' => \App\Support\ModuleHelper::isSystemEnabled(),
'modules' => \App\Support\ModuleHelper::getAllModules(),
],
]);
}
/**
* Update the module override map. Accepts an associative array of
* { module_key: bool|null }. A null value clears that key's override
* so the env/config default takes effect again.
*/
public function updateModules(Request $request)
{
if (!Auth::user()->isUltimate()) {
return ResponseHelper::returnUnauthorized();
}
$validated = $request->validate([
'overrides' => 'required|array',
]);
$configModules = (array) \Hypervel\Support\Facades\Config::get('modules', []);
$current = SystemSetting::getValue(\App\Support\ModuleHelper::OVERRIDE_KEY);
if (is_string($current) && $current !== '') {
$decoded = json_decode($current, true);
$current = is_array($decoded) ? $decoded : [];
} elseif (!is_array($current)) {
$current = [];
}
foreach ($validated['overrides'] as $key => $value) {
// Only allow keys that are actually defined as modules.
if (!isset($configModules[$key]) || !is_array($configModules[$key])) {
continue;
}
if ($value === null || $value === 'null') {
unset($current[$key]);
} else {
$current[$key] = filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
}
try {
SystemSetting::setValue(
\App\Support\ModuleHelper::OVERRIDE_KEY,
json_encode((object) $current),
'modules',
'json'
);
SystemSetting::clearCache();
return response()->json([
'success' => true,
'message' => 'Module states updated',
'data' => [
'modules' => \App\Support\ModuleHelper::getAllModules(),
],
]);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Failed to update modules: ' . $e->getMessage(),
], 500);
}
}
/**
* Get public settings for frontend.
*/
public function getPublicSettings()
{
return response()->json([
'success' => true,
'data' => self::getPublicSettingsData()
]);
}
}

View File

@@ -0,0 +1,412 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Hypervel\Http\Request;
use App\Models\FileList;
use App\Models\FileContent;
use Hypervel\Http\UploadedFile;
use Hypervel\Support\Facades\File;
use Hypervel\Support\Facades\Storage;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\DB;
use \PDO;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Carbon;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\Permissions\ProductPermissions;
use App\Enums\UserActions;
class FilesMainController
{
private static function isLikelyBinary($string, $threshold = 0.3)
{
// Null byte check: very likely binary
if (strpos($string, "\x00") !== false) {
return true;
}
// If string is empty, consider it not binary
$len = strlen($string);
if ($len === 0)
return false;
// Count non-printable characters
$nonPrintable = preg_match_all('~[^\x09\x0A\x0D\x20-\x7E]~', $string);
// If more than $threshold of the content is non-printable, likely binary
return ($nonPrintable / $len) > $threshold;
}
private static function insertFileContentPostgresPDO(array $data)
{
$pdo = DB::getPdo();
$stmt = $pdo->prepare('
INSERT INTO file_content
(hashkey, filehash, titlename, description, size_in_bytes, content, filelocation, created_by, updated_by, details, created_at, updated_at)
VALUES
(:hashkey, :filehash, :titlename, :description, :size_in_bytes, :content, :filelocation, :created_by, :updated_by, :details, :created_at, :updated_at)
');
$now = now()->toDateTimeString();
$stmt->bindParam(':hashkey', $data['hashkey']);
$stmt->bindParam(':filehash', $data['filehash']);
$stmt->bindParam(':titlename', $data['titlename']);
$stmt->bindParam(':description', $data['description']);
$stmt->bindParam(':size_in_bytes', $data['size_in_bytes']);
$stmt->bindParam(':content', $data['content'], PDO::PARAM_LOB);
$stmt->bindParam(':filelocation', $data['filelocation']);
$stmt->bindParam(':created_by', $data['created_by']);
$stmt->bindParam(':updated_by', $data['updated_by']);
$detailsJson = json_encode($data['details']);
$stmt->bindParam(':details', $detailsJson);
$stmt->bindParam(':created_at', $now);
$stmt->bindParam(':updated_at', $now);
$stmt->execute();
// Optionally, get the inserted ID and fetch the model
$id = DB::getPdo()->lastInsertId();
return FileContent::find($id);
}
private static function insertFileContentSql(array $data)
{
$now = Carbon::now();
$data['content'] = base64_encode($data['content']);
$data['details'] = json_encode($data['details']);
$data['created_at'] = $now;
$data['updated_at'] = $now;
return FileContent::create($data);
}
private static function insertFileContent(array $data)
{
$driver = DB::connection()->getDriverName();
if ($driver === 'pgsql') {
return self::insertFileContentPostgresPDO($data);
}
if ($driver === 'mysql') {
return self::insertFileContentSql($data);
}
throw new \RuntimeException("Unsupported database driver: {$driver}");
}
/**
* Upload file content and store metadata in DB.
*
* @param \Hypervel\Http\UploadedFile|string $fileData // filepath, filebinaryData, UploadedFileRequest
* @param string $title
* @param string|null $description
* @param string|null $filelocation
* @param array|null $details
*/
private static function uploadFileContent(UploadedFile|string $fileData, string $title, ?string $description = null, ?array $details = [])
{
if ($fileData instanceof UploadedFile) {
$fileHash = hash_file('sha256', $fileData->getRealPath());
$fileSize = $fileData->getSize();
$filename = $fileHash;
$fileContent = File::get($fileData->getRealPath());
$path = $fileData->storeAs('files', $filename);
} elseif (is_string($fileData) && file_exists($fileData)) {
$fileHash = hash_file('sha256', $fileData);
$fileSize = filesize($fileData);
$fileContent = file_get_contents($fileData);
$path = Storage::put("files/{$fileHash}", $fileContent);
} elseif (self::isLikelyBinary($fileData)) {
$fileHash = hash('sha256', $fileData);
$fileSize = strlen($fileData);
// $path = Storage::putFile('files', $fileData);
$fileContent = $fileData;
$path = Storage::put("files/{$fileHash}", $fileContent);
} else {
throw new \InvalidArgumentException('Invalid file data provided.');
}
$hashKey = hash('sha256', uniqid((string) now(), true));
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimetype = $finfo->buffer($fileContent);
// $fileContent = FileContent::create([
// 'hashkey' => $hashKey,
// 'filehash' => $fileHash,
// 'titlename' => $title,
// 'description' => $description,
// 'size_in_bytes' => $fileSize,
// 'content' => $fileContent,
// 'filelocation' => $filelocation ?? $path,
// 'created_by' => Auth::id(),
// 'updated_by' => Auth::id(),
// 'details' => $details ?? [],
// ]);
$fileContentDB = FileContent::where('filehash', $fileHash)->first();
if ($fileContentDB) {
return $fileContentDB;
}
$fileContentDB = self::insertFileContent([
'hashkey' => $hashKey,
'filehash' => $fileHash,
'titlename' => $title,
'description' => $description,
'size_in_bytes' => $fileSize,
'content' => $fileContent,
'filelocation' => $filelocation ?? $path,
'created_by' => Auth::id(),
'updated_by' => Auth::id(),
'details' => $details ?? [],
'mimetype' => $mimetype
]);
return $fileContentDB;
}
private static function insertFileList(int $contentuid, string $title, string $filename, string $description, $categories, $details, $tags = [], $hidden = 0, ?string $file_type = null)
{
$filecontent_exists = FileContent::where('id', $contentuid)->exists();
if (!$filecontent_exists) {
throw new \Exception("File Content Does not Exist", 1);
}
$data = [
'contentuid' => $contentuid,
'hashkey' => hash('sha256', uniqid((string) now(), true)),
'title' => $title,
'filename' => $filename,
'description' => $description,
'categories' => $categories,
'details' => $details,
'tags' => $tags,
'hidden' => $hidden,
'file_type' => $file_type,
'is_public' => false,
'created_by' => Auth::id(),
'updated_by' => Auth::id(),
];
$filelist = FileList::create($data);
return $filelist;
}
public static function uploadFileList(string|UploadedFile $fileData, string $title, string $filename, ?string $description = null, ?array $details = [], $categories = null, $tags = [], $hidden = 0, ?string $file_type = null)
{
try {
$fileContent = self::uploadFileContent($fileData, $title, $description, $details);
$filecontent_id = $fileContent->id;
// print_r($fileContent);
return self::insertFileList($filecontent_id, $title, $filename, $description, $categories, $details, $tags, $hidden, $file_type);
} catch (\Throwable $th) {
return Response::json($th->getMessage(), 500);
// return Response::make('Error uploading file content: ' . $th->getMessage(), 500);
return false;
}
}
public static function viewFilebyFileListHash(string $filelist_hash)
{
$filelist = FileList::where('hashkey', $filelist_hash)->first();
// return Response::json($filelist, 500);
if (!$filelist) {
abort(404, 'File not found');
}
$cdnUrl = trim((string) ($filelist->cdn_url ?? ''));
if ($cdnUrl !== '') {
return redirect($cdnUrl);
}
$fileContent = $filelist->fileContent;
if (!$fileContent) {
abort(404, 'File content not found');
}
$content = $fileContent->content;
$driver = DB::connection()->getDriverName();
if (is_resource($content) && $driver !== 'mysql') {
$content = stream_get_contents($content);
}
if ($driver === 'mysql') {
$content = base64_decode($content);
}
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->buffer($content);
// Map common MIME types to extensions
$mimeMap = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/gif' => 'gif',
'image/webp' => 'webp',
'application/pdf' => 'pdf',
'text/plain' => 'txt',
'text/html' => 'html',
'application/zip' => 'zip',
'application/json' => 'json',
'audio/mpeg' => 'mp3',
'video/mp4' => 'mp4',
// add more as needed
];
// Pick extension from map, or fallback
$extension = $mimeMap[$mimeType] ?? pathinfo($filelist->filename, PATHINFO_EXTENSION);
// Build safe filename with extension
$filename = pathinfo($filelist->filename, PATHINFO_FILENAME) . '.' . $extension;
return Response::make($content, 200, [
'Content-Type' => $mimeType,
'Content-Disposition' => 'inline; filename="' . $filename . '"',
'Content-Length' => strlen($content),
]);
// return Response::make($content, 200, [
// 'Content-Type' => 'application/octet-stream',
// 'Content-Disposition' => 'inline; filename="' . $filelist->filename . '"',
// 'Content-Length' => strlen($content),
// ]);
}
private static function canUploadCategory(string $category): bool
{
if (!Auth::check()) {
return false;
}
if (UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::UploadAllFiles)) {
return true;
}
if (strcasecmp($category, 'ProductMarket') === 0) {
return ProductPermissions::isActionAllowed(UserActions::CreateProductForOwnStore)
|| ProductPermissions::isActionAllowed(UserActions::AddProducttoOwnStore);
}
return false;
}
public static function UploadFilefromRequest(Request $request, string $category)
{
if (!self::canUploadCategory($category)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
if (!$request->hasFile('file')) {
// $responseData =['success' => false, 'message' => 'No file uploaded'];
$responseData = false;
return response()->json(false, 400);
}
$file = $request->file('file');
$filename = $file->getClientFilename();
$result = self::uploadFileList(
$file,
'',
$filename ?? '',
'',
[],
$category,
[],
0,
null,
);
// return response()->json(['success' => false, 'error' => 'File upload failed'], 500);
// return $result;
$file_url = $result->resolvedUrl();
try {
if (is_string($result->hashkey) && !empty($result->hashkey)) {
return response()->json(['success' => true, 'hashkey' => $result->hashkey, 'message' => 'File uploaded successfully', 'url' => $file_url, 'name' => $filename], 200);
} else {
throw new \Exception("File upload failed", 1);
}
} catch (\Throwable $th) {
return response()->json(['success' => false, 'error' => 'File upload failed'], 500);
}
}
public static function generateURLforFileListHash(string $filelist_hashkey)
{
$cdnUrl = FileList::where('hashkey', $filelist_hashkey)->value('cdn_url');
if (is_string($cdnUrl) && trim($cdnUrl) !== '') {
return $cdnUrl;
}
return "/RequestData/File/$filelist_hashkey";
// return route('requestdata.file.view', ['hash' => $filelist_hashkey]);
}
public static function bladePreloadFileScript(string|FileList $FileHash): null|string
{
try {
if ($FileHash instanceof FileList) {
$filecontent = $FileHash->fileContent->content;
$mimeType = $FileHash->fileContent->mimetype ?? 'application/octet-stream';
$FileHash = $FileHash->hashkey;
} else {
$FileHash = FileList::where('hashkey', $FileHash)->firstOrFail();
$filecontent = $FileHash->fileContent->content;
$mimeType = $FileHash->fileContent->mimetype;
$FileHash = $FileHash->hashkey;
}
} catch (\Throwable $th) {
return '';
}
$base64 = base64_encode($filecontent);
$htmlString = "reqcacheupdateBase64toFile('$FileHash','$base64','$mimeType')";
return $htmlString;
}
}

View File

@@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Enums\Market\ProductTransactionType;
use App\Enums\Market\TransactionFlow;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\GlobalTransaction;
use App\Models\Market\Product;
use App\Models\Market\Store;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Validator;
use App\Http\Controllers\Helpers\UserController;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
class GlobalTransactionController
{
/**
* List transactions with optional filtering.
* Handles POST /admin/transactions/list
*/
public function list(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewGlobalTransactions)) {
return ResponseHelper::returnUnauthorized();
}
$user = Auth::user();
$productIdHash = $request->input('product_id');
$storeIdHash = $request->input('store_id');
$query = GlobalTransaction::with(['user:id,name,hashkey', 'product:id,name,hashkey', 'store:id,name,hashkey']);
if ($productIdHash) {
$product = Product::where('hashkey', $productIdHash)->first();
if ($product) {
$query->where('product_id', $product->id);
} else {
return response()->json([]);
}
}
if ($storeIdHash) {
$store = Store::where('hashkey', $storeIdHash)->first();
if ($store) {
$query->where('store_id', $store->id);
}
}
// Access Control: Ultimate can see everything.
// Others see transactions they created or relate to them.
if ($user->acct_type !== UserTypes::ULTIMATE) {
$query->where(function($q) use ($user) {
$q->where('user_id', $user->id)
->orWhere('created_by', $user->id);
});
}
$transactions = $query->orderBy('created_at', 'desc')->get();
// Transform for frontend
$transformed = $transactions->map(function ($tx) {
return [
'id' => $tx->id,
'hashkey' => $tx->hashkey,
'amount' => $tx->amount,
'type' => $tx->type ? $tx->type->label() : 'Unknown',
'status' => $tx->status,
'description' => $tx->description,
'created_at' => $tx->created_at,
'flow' => $tx->flow ? [
'value' => $tx->flow->value,
'label' => match($tx->flow) {
TransactionFlow::INCOME => 'Income',
TransactionFlow::EXPENSE => 'Expense',
TransactionFlow::NEUTRAL => 'Neutral',
default => 'Unknown'
}
] : null,
'user' => $tx->user ? $tx->user->name : null,
'product' => $tx->product ? $tx->product->name : null,
'store' => $tx->store ? $tx->store->name : null,
];
});
return response()->json($transformed);
}
/**
* Create a new global transaction.
* Handles POST /admin/transactions/create
*/
public function store(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateGlobalTransaction)) {
return ResponseHelper::returnUnauthorized();
}
$user = Auth::user();
$validator = Validator::make($request->all(), [
'user_hash' => 'nullable|string',
'amount' => 'required|numeric',
'type' => 'required|integer',
'description' => 'nullable|string',
'product_hash' => 'nullable|string',
'store_hash' => 'nullable|string',
'status' => 'nullable|string',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$data = $validator->validated();
// Resolve IDs from hashes
$targetUserId = $user->id;
if (isset($data['user_hash'])) {
$targetUser = UserController::findUserIdByHash($data['user_hash']);
if ($targetUser) $targetUserId = $targetUser;
}
$productId = null;
if (isset($data['product_hash'])) {
$product = Product::where('hashkey', $data['product_hash'])->first();
if ($product) $productId = $product->id;
}
$storeId = null;
if (isset($data['store_hash'])) {
$store = Store::where('hashkey', $data['store_hash'])->first();
if ($store) $storeId = $store->id;
}
/** @var GlobalTransaction $transaction */
$transaction = GlobalTransaction::create([
'user_id' => $targetUserId,
'amount' => $data['amount'],
'type' => $data['type'],
'description' => $data['description'] ?? '',
'product_id' => $productId,
'store_id' => $storeId,
'status' => $data['status'] ?? 'completed',
'flow' => $request->has('flow') ? $request->input('flow') : ProductTransactionType::from($data['type'])->flow(),
'created_by' => $user->id,
]);
return response()->json([
'success' => true,
'message' => 'Transaction recorded successfully',
'hashkey' => $transaction->hashkey
]);
}
/**
* Get all product transaction types for dropdown selection.
*/
public function getTypes()
{
$types = collect(ProductTransactionType::cases())->map(function ($type) {
return [
'value' => $type->value,
'label' => $type->label(),
'flow' => $type->flow()->value,
'flow_label' => $type->flow()->name
];
});
return response()->json($types);
}
}

View File

@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Helpers;
use Hyperf\Coroutine\Coroutine;
use Hypervel\Support\Facades\Cache;
use Hyperf\Database\Model\Builder as ModelBuilder;
use Hyperf\Database\Query\Builder as QueryBuilder;
use Hypervel\Database\Eloquent\Model;
class CacheHelper
{
/**
* Get data from cache.
*
* @param mixed $query Can be SQL string or Builder object
* @param mixed $params Parameters to hash (array, object, or model)
* @return mixed|null
*/
public static function get_cache($query, $params = [])
{
try {
$key = self::generateKey($query, $params);
return Cache::get($key);
} catch (\Throwable $e) {
return null;
}
}
/**
* Set data in cache using a background coroutine (fire-and-forget).
*
* @param mixed $query Can be SQL string or Builder object
* @param mixed $params Parameters to hash
* @param mixed $data Data to cache
* @param int $ttl Time to live in seconds (default: 1 day)
*/
public static function set_cache($query, $data, $params = [], int $ttl = 86400)
{
$key = self::generateKey($query, $params);
// Fire-and-forget coroutine: we don't wait for completion
Coroutine::create(function () use ($key, $data, $ttl) {
try {
Cache::put($key, $data, $ttl);
} catch (\Throwable $e) {
// Silently fail as cache operations are non-critical
}
});
}
/**
* Erase a specific cache entry.
*
* @param mixed $query Can be SQL string or Builder object
* @param mixed $params Parameters to hash
*/
public static function erase_cache($query, $params = [])
{
try {
$key = self::generateKey($query, $params);
Cache::forget($key);
} catch (\Throwable $e) {
// Silently fail
}
}
/**
* Generate the cache key using the specific format:
* querycache:[queryhash]++++[parameterhash]
*/
public static function generateKey($query, $params): string
{
$sql = '';
$parameterData = $params;
// Automatically handle Builder objects
if ($query instanceof ModelBuilder || $query instanceof QueryBuilder) {
$sql = $query->toSql();
// If explicit params were not provided or empty, use the builder's bindings
if (empty($params)) {
$parameterData = $query->getBindings();
}
} else {
$sql = (string) $query;
}
$queryHash = md5($sql);
$paramHash = md5(self::serializeParams($parameterData));
return "querycache:{$queryHash}++++{$paramHash}";
}
/**
* Deep serialization of parameters for hashing.
* Supports arrays, objects, and models.
*/
private static function serializeParams($params): string
{
if (is_array($params)) {
$processed = [];
foreach ($params as $key => $value) {
$processed[$key] = self::serializeParams($value);
}
// Sort keys to ensure consistent hashing for the same associative array
ksort($processed);
return json_encode($processed);
}
if (is_object($params)) {
// Handle models specifically
if ($params instanceof Model) {
return get_class($params) . ':' . ($params->hashkey ?? $params->id ?? 'new');
}
if (method_exists($params, 'toArray')) {
return json_encode($params->toArray());
}
return get_class($params) . ':' . spl_object_hash($params);
}
return (string) $params;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,304 @@
<?php
//File Content
class DB_FILE_CONTENT
{
public $DB = false;
public $tablename = 'file_content';
use BASICDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
function NewFileContent($file, $filename = '', $toDB = true)
{
if (!file_exists($file)) {
return false;
}
if (!$filename) {
$filename = basename($file);
}
$table = 'file_content';
$filecontent = file_get_contents($file);
$filehash = hash_file('sha256', $file);
$size = filesize($file);
$ifexists = DBFunctions($this->tablename, $this->DB)->CheckifUIDorHashKeyExist($filehash, $fieldstoselectarray = ['uid', 'hashkey'])['uid'] ?? false;
if ($ifexists) {
return $ifexists;
}
global $DB;
if (!$DB) {
$DB = DB();
}
$data = ['titlename' => $filename, 'hashkey' => $filehash, 'size_in_bytes' => $size];
if ($toDB === true) {
$data['content'] = $filecontent;
} elseif (!$toDB) {
$toDB = 'files/';
} else {
if (!is_dir($toDB)) {
return false;
}
}
if (!endsWithSlash($toDB)) {
$toDB .= "/";
}
if ($toDB !== true) {
file_put_contents($toDB . $filehash, $filecontent);
if (!file_exists($toDB . $filehash) or filesize($toDB . $filehash) !== $size) {
return false;
}
$data['filelocation'] = $toDB;
$data['content'] = '';
}
$newuid = insertintodb($DB, $table, $data);
return $newuid;
}
function DeleteFileContent($uidorhashkey)
{
$where = [];
if (!$uidorhashkey) {
return false;
}
if (is_numeric($uidorhashkey)) {
$where['uid'] = $uidorhashkey;
} else {
$where['hashkey'] = $uidorhashkey;
}
return deletefromdb('file_content', $where);
}
function getFileContentDetails($uidorhashkey, $fieldstoselect = '')
{
return getDetailsbyUIDorHashkey('file_content', $uidorhashkey, $fieldstoselect);
}
function getFileContentUIDbyHashkey($hashkey)
{
if (!$hashkey or is_numeric($hashkey)) {
return false;
}
return $this->getFileContentDetails($hashkey, ['uid', 'hashkey'])['uid'] ?? false;
}
function getFileContentHashkeybyUID($UID)
{
if (!$UID or !is_numeric($UID)) {
return false;
}
return $this->getFileContentDetails($UID, ['uid', 'hashkey'])['hashkey'] ?? false;
}
}
function DB_FILE_CONTENT($DB = false)
{
return new DB_FILE_CONTENT($DB);
}
class FILE_CONTENT_QUICKMULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'file_content';
private $parentidresults = [];
public $DB;
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false, $DB = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata, $DB);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
}
function FILE_CONTENT_QUICKMULTIPLESEARCH($DB = false)
{
return new FILE_CONTENT_QUICKMULTIPLESEARCH($DB);
}
class DB_FILE_LIST
{
public $DB = false;
public $tablename = 'file_list';
use BASICDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
function insertFileListintoDB($contentuid, $filename, $description = '', $tags = '', $categories = '', $added = '', $addedby = '', $hidden = 0)
{
if (!$contentuid or !$filename) {
return false;
}
if (!$addedby) {
global $CurrentUserUID;
$addedby = $CurrentUserUID;
if (!$addedby) {
$addedby = CurrentUserUID();
}
}
if (!$added) {
$added = serverdatetimesql();
}
$modified = serverdatetimesql();
if (!$description) {
$description = '';
}
if (!$tags) {
$tags = '';
}
if (!$categories) {
$categories = '';
}
$hash = generatenewhash($this->tablename);
$categories = tryjsonencode($categories);
$datenow = serverdatetimesql();
$tags = tryjsonencode($tags);
$data = [
'hashkey' => $hash,
'contentuid' => $contentuid,
'useruid_access_list' => tryjsonencode(''),
'filename' => $filename,
'description' => $description,
'tags' => $tags,
'categories' => $categories,
'created' => $added,
'modified' => $modified,
'hidden' => $hidden,
'addedby' => $addedby
];
global $DB;
if (!$DB) {
$DB = DB();
}
$key = insertintodb($DB, 'file_list', $data);
return $key;
}
function getFileListDetails($uidorhashkey, $fieldstoselect = '')
{
return getDetailsbyUIDorHashkey($this->tablename, $uidorhashkey, $fieldstoselect);
}
function getFileListUIDbyHashkey($hashkey)
{
if (!$hashkey or is_numeric($hashkey)) {
return false;
}
return $this->getFileListDetails($hashkey, ['uid', 'hashkey'])['uid'] ?? false;
}
function getFileListHashkeybyUID($UID)
{
if (!$UID or !is_numeric($UID)) {
return false;
}
return $this->getFileListDetails($UID, ['uid', 'hashkey'])['hashkey'] ?? false;
}
function InsertFileListandFileContentFromFile($filelocation, $filename, $toDBtrueifFalseSateLocationtoSave = false, $description = '', $tags = '', $categories = '', $hidden = 0)
{
if (!$filelocation and !$filename) {
return false;
}
$contentuid = DB_FILE_CONTENT()->NewFileContent($filelocation, $filename, $toDBtrueifFalseSateLocationtoSave);
$filelistuid = $this->insertFileListintoDB($contentuid, $filename, $description, $tags, $categories, $added = '', $addedby = '', $hidden);
return $filelistuid;
}
}
function DB_FILE_LIST($DB = false)
{
return new DB_FILE_LIST($DB);
}
class FILE_LIST_QUICKMULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'file_list';
private $parentidresults = [];
public $DB;
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false, $DB = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata, $DB);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
}
function FILE_LIST_QUICKMULTIPLESEARCH($DB = false)
{
return new FILE_CONTENT_QUICKMULTIPLESEARCH($DB);
}

View File

@@ -0,0 +1,368 @@
<?php
$classMap['STORES'] = 'DB_STORES';
$classMap['PRODUCTS'] = 'DB_PRODUCTS';
$classMap['PRODUCTS_TRANSACTIONS'] = 'DB_PRODUCTS_TRANSACTIONS';
$classMap['POS_TRANSACTIONS'] = 'DB_POS_TRANSACTIONS';
$classMap['PRODUCTS_TRANSACTIONS_SESSIONS'] = 'DB_PRODUCTS_TRANSACTIONS_SESSIONS';
$classMap['PRODUCTS_HISTORY'] = 'DB_PRODUCTSHISTORY';
$classMap['CART'] = 'DB_CART';
$classMap['SUPPLIERS'] = 'DB_SUPPLIERS';
$classMap['SUPPLIERS'] = 'DB_SUPPLIERS';
class DB_CART extends DB_USERS
{
private function USERS()
{
return new DB_USERS();
}
public function __construct()
{
parent::__construct();
}
function getCartContents($uidorhashkey)
{
if (!$uidorhashkey) {
return false;
}
$DB = $this->DB;
$wherearray = [];
if (is_numeric($uidorhashkey)) {
$wherearray = ['uid' => $uidorhashkey];
} else {
$wherearray = ['hashkey' => $uidorhashkey];
}
$DBQUERY = $this->USERS()->GetUserDatabyUID($uidorhashkey);
if (!$DBQUERY) {
return false;
}
$result = tryjsondecode($DBQUERY['cart']) ?? [];
return $result;
}
function getCartContentsAndComputations($uidorhashkey){
$cartContents= $this->getCartContents($uidorhashkey);
if (!$cartContents){return false;}
return $this->getCartDetailsComputations($cartContents);
}
function getCartDetailsComputations($cartdata)
{
if (!$cartdata || !is_array($cartdata)) {
return false;
}
$productsClass = new DB_PRODUCTS_QUICK_MULTIPLESEARCH;
$storeClass = new DB_STORES_QUICK_MULTIPLESEARCH;
$CartDetails = [];
$cartproducts = [];
foreach ($cartdata as $value) {
$cart_product_hashkey = $value['hashkey'] ?? $value[0];
$cart_product_quantity = $value['quantity'] ?? $value[1];
$cart_product_added = $value['added'] ?? $value[2];
$cart_product_modified = $value['modified'] ?? $value[3];
$productdata = $productsClass->getDetailsbyUIDorHashkey($cart_product_hashkey) ?? false;
if (!$productdata) {
continue;
}
$product_active = $productdata['status'] === 'active';
$product_available = $productdata['available'] >= $cart_product_quantity;
$productstore_uid = $productdata['storeuid'];
$productstore_hash = $storeClass->getHASHfromUID($productstore_uid);
$productphoto_array = tryjsondecode($productdata['photourl']);
if (!is_array($productphoto_array)) {
$productphotoarray = [];
}
$productphotourl = $productphoto_array[0] ?? null;
$productprice = $productdata['price'];
$product_unit = $productdata['unitname'];
$product_name = $productdata['name'];
if ($cart_product_quantity < 1) {
return false;
}
$cartproducts[] = [
'hashkey' => $cart_product_hashkey,
'quantity' => $cart_product_quantity,
'price' => $productprice,
'unit' => $product_unit,
'name' => $product_name,
'photourl' => $productphotourl,
'added' => $cart_product_added,
'modified' => $cart_product_modified,
'store' => $productstore_hash,
'active' => $product_active,
'available' => $product_available,
'subtotal' => $productprice * $cart_product_quantity
];
}
$total = array_sum(array_column($cartproducts, 'subtotal'));
if ($total < 2) {
return false;
}
$CartDetails['details'] = [
'total' => $total,
'shipping' => 'Unknown'
];
$CartDetails['cart'] = $cartproducts;
return $CartDetails;
}
function SetCartContents($uidorhashkey, $newcontentsArray)
{
if (!$uidorhashkey) {
return false;
}
$DB = $this->DB;
$newcontentsArray = tryjsonencode($newcontentsArray);
$newcontentsArray = ['cart' => $newcontentsArray];
$DBNow = DBQUERY()->USERS()->ModifyUser($newcontentsArray, $uidorhashkey);
return $DBNow;
}
function AddCartContentsSingleProduct($uidorhashkey, $producthashkey, $additionalquantity)
{
if (!$uidorhashkey) {
return false;
}
$exists = DBQUERY()->USERS()->GetUserDatabyUID($uidorhashkey);
if (!$exists) {
return false;
}
if (!$producthashkey || is_numeric($producthashkey)) {
return false;
}
if (!$additionalquantity || !is_numeric($additionalquantity)) {
return false;
}
$contents = $this->getCartContents($uidorhashkey);
if ($contents) {
$producthashkeys = array_column($contents, 0);
} else {
$contents = [];
$producthashkeys = [];
}
$targetkey = array_search($producthashkey, $producthashkeys);
if ($targetkey === false) {
$contents[] = [$producthashkey, $additionalquantity, serverdatetimesql(), serverdatetimesql()];
} else {
$content_details = $contents[$targetkey];
$currentquantity = $content_details[1];
$currentdate_added = $content_details[2];
$newdate_modfied = serverdatetimesql();
$newquantity = $currentquantity + $additionalquantity;
if ($newquantity < 0) {
return false;
}
$contents[$targetkey] = [$producthashkey, $newquantity, $currentdate_added, $newdate_modfied];
}
return $this->SetCartContents($uidorhashkey, $contents);
}
function EmptyCartContents($uidorhashkey)
{
if (!$uidorhashkey) {
return false;
}
return $this->SetCartContents($uidorhashkey, []);
}
function DeleteCartContents($uidorhashkey, $producthashkey)
{
if (!$uidorhashkey) {
return false;
}
if (!$producthashkey || is_numeric($producthashkey)) {
return false;
}
$contents = $this->getCartContents($uidorhashkey);
$producthashkeys = array_column($contents, 0);
$targetkey = array_search($producthashkey, $producthashkeys);
if ($targetkey === false) {
unset($contents[$targetkey]);
$contents = array_values($contents);
}
return $this->SetCartContents($uidorhashkey, $contents);
}
function CheckifProductisAllowedtoAddtoCart($producthashkeyoruid, $quantity)
{
//returns the product details if allowed, false if not
$ProductDetails = DB_PRODUCTS()->getDetailsbyUIDorHashkey($producthashkeyoruid);
if (!$ProductDetails || $ProductDetails['status'] !== 'active' || $ProductDetails['available'] < 1) {
return false;
}
if ($quantity > $ProductDetails['available']) {
return false;
}
return $ProductDetails;
}
function SetProductQuantityCartContent($uidorhashkey, $producthashkeyoruid, $quantity)
{
if (!$uidorhashkey || !$producthashkeyoruid || $quantity === null) {
return false;
}
$producthashkeyifAllowed = $this->CheckifProductisAllowedtoAddtoCart($producthashkeyoruid, $quantity)['hashkey'] ?? false;
if (!$producthashkeyifAllowed) {
return false;
}
$producthashkeyoruid = $producthashkeyifAllowed;
$contents = $this->getCartContents($uidorhashkey);
if (!$contents) {
$contents = [];
}
$producthashcolumns = array_column($contents, 0);
$targetkey = array_search($producthashkeyoruid, $producthashcolumns);
if ($targetkey === false) {
$contents[] = [$producthashkeyoruid, $quantity, serverdatetimesql(), serverdatetimesql()];
} else {
$content_details = $contents[$targetkey];
$currentquantity = $content_details[1];
$currentdate_added = $content_details[2];
$newdate_modfied = serverdatetimesql();
$contents[$targetkey] = [$producthashkeyoruid, $quantity, $currentdate_added, $newdate_modfied];
}
$updatecontent = $this->SetCartContents($uidorhashkey, $contents);
return $updatecontent;
}
function ShowCartStructure()
{
$structure = ['contenthashkey', 'quantity', 'date_added', 'date_modified'];
e($structure);
}
}
function CartDB($DB = false)
{
return new DB_CART($DB);
}
class MARKET_MAIN
{
public $DB = false;
public function __construct($DB = false)
{
}
function isNewTransactionAllowed($createdby_details, $createdfor_details, $ownerdetails)
{
if (!$createdby_details || !$createdfor_details || !$ownerdetails) {
return false;
}
$parentflagAllowed = false;
$acct_typeflagAllowed = false;
$ownerUID = $ownerdetails['uid'] ?? false;
if (!$ownerUID) {
return false;
}
if (!is_array($createdby_details)) {
$createdby_type = $createdby_details;
} else {
$createdby_type = $createdby_details['acct_type'];
}
if (!is_array($createdby_details)) {
$createdfor_type = $createdfor_details;
} else {
$createdfor_type = $createdfor_details['acct_type'];
}
$createdby_Allowed_Types = $createdby_type === 'storeowner' || $createdby_type === 'storemanager' || $createdby_type === 'ult' || $createdby_type === 'superoperator';
$createdfor_Allowed_Types = $createdby_type === 'storeowner' || $createdby_type === 'storemanager';
$iscreatedbyULT = $createdby_type === 'ult';
$createdfor_Parent = $createdfor_details['parentuid'];
$createdby_Parent = $createdby_details['parentuid'];
if ($createdfor_Parent === $ownerUID) {
$parentflagAllowed = true;
}
if ($createdby_Allowed_Types && $createdfor_Allowed_Types) {
$acct_typeflagAllowed = true;
}
if ($iscreatedbyULT) {
return true;
}
if ($acct_typeflagAllowed && $parentflagAllowed) {
return true;
} else {
return false;
}
}
function getCityFromAddress($address)
{
$citiesfile = file_get_contents('lib/cities.json');
$cities = json_decode($citiesfile) ?? false;
$cities = array_column($cities, 'name') ?? false;
if (!$cities) {
return false;
}
foreach ($cities as $city) {
if (stripos($address, $city) !== false) {
return $city;
}
}
return null;
}
}
function MARKET_MAIN()
{
return new MARKET_MAIN();
}
require_once('MarketDB/products.db.php');
require_once('MarketDB/productshistory.db.php');
require_once('MarketDB/product_transactions.db.php');
require_once('MarketDB/product_transactions_sessions.db.php');
require_once('MarketDB/product_transactions_sessions.db.php');
require_once('MarketDB/pos_transactions.db.php');
require_once('MarketDB/pos_transactions_sessions.db.php');
require_once('MarketDB/stores.db.php');
require_once('MarketDB/suppliers.db.php');
?>

View File

@@ -0,0 +1,75 @@
<?php
class DB_POS_TRANSACTIONS
{
public $DB = false;
public $tablename = 'pos_transactions';
use BASICDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
function NewPOSTransaction()
{
$data = [
];
return $this->InsertBasicDBHashCreatedModified($data);
}
}
class DB_POS_TRANSACTIONS_QUICK_MULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'pos_transactions';
private $parentidresults = [];
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
}
function DB_POS_TRANSACTIONS($DB = false)
{
return new DB_POS_TRANSACTIONS($DB);
}
function DB_POS_TRANSACTIONS_QUICK_MULTIPLESEARCH($DB = false)
{
return new DB_POS_TRANSACTIONS_QUICK_MULTIPLESEARCH($DB);
}

View File

@@ -0,0 +1,61 @@
<?php
class DB_POS_TRANSACTIONS_SESSIONS
{
public $DB = false;
public $tablename = 'pos_transactions_sessions';
use BASICDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
function NewRecord()
{
}
}
class DB_POS_TRANSACTIONS_SESSIONS_QUICK_MULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'pos_transactions_sessions';
private $parentidresults = [];
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
}
function DB_POS_TRANSACTIONS_SESSIONS($DB = false)
{
return new DB_POS_TRANSACTIONS_SESSIONS($DB);
}
function DB_POS_TRANSACTIONS_SESSIONS_QUICK_MULTIPLESEARCH($DB = false)
{
return new DB_POS_TRANSACTIONS_SESSIONS_QUICK_MULTIPLESEARCH($DB);
}

View File

@@ -0,0 +1,146 @@
<?php
class DB_PRODUCTS_TRANSACTIONS
{
public $DB = false;
public $tablename = 'products_transactions';
use BASICDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
function NewProductTransactions($productuid, $storeuid, $quantity, $transactiontype, $transactionsessionhash = '', $subtype = '', $createdfor = '', $price = '', $owneruid = '', $createdby = '', $name = '', $description = '', $transactiondata = '', $remarks = '')
{
if (!$productuid || !$storeuid || !$transactiontype || !$quantity) {
return false;
}
global $CurrentUserUID;
$createdby = $CurrentUserUID;
if (!$createdby) {
$createdby = CurrentUserUID();
}
if (!$createdfor) {
$createdfor = $createdby;
}
$data = [];
$AddtoDataifNOTEmpty = function ($varname, $varvalue) use (&$data) {
if (!$varname || !$varvalue) {
return false;
}
if (is_array($varvalue)) {
$varvalue = tryjsonencode($varvalue);
}
$data[$varname] = $varvalue;
};
$date = serverdatetimesql();
$created = $date;
$modified = $date;
if (!$transactionsessionhash) {
$transactionsessionhash = DBQUERY()->PRODUCTS_TRANSACTIONS_SESSIONS()->NewProductsTransactionSession($storeuid);
} else {
if (!DBQUERY()->PRODUCTS_TRANSACTIONS_SESSIONS()->checkifUIDorHashKeyexist($transactionsessionhash)) {
return false;
}
}
if (!$transactiondata) {
$transactiondata = [];
}
$storeexists = DBQUERY()->STORES()->CheckifUIDorHashKeyExist($storeuid);
if (!$storeexists) {
return false;
}
$productexists = DBQUERY()->PRODUCTS()->CheckifUIDorHashKeyExist($productuid);
if (!$productexists) {
return false;
}
$logs = [$date, 'Created New Product Transaction by useruid(' . $createdby . ') at ' . $modified . ' .'];
$AddtoDataifNOTEmpty('productuid', $productuid);
$AddtoDataifNOTEmpty('storeuid', $storeuid);
$AddtoDataifNOTEmpty('quantity', $quantity);
$AddtoDataifNOTEmpty('transactiontype', $transactiontype);
$AddtoDataifNOTEmpty('transactionsessionhash', $transactionsessionhash);
$AddtoDataifNOTEmpty('subtype', $subtype);
$AddtoDataifNOTEmpty('createdfor', $createdfor);
$AddtoDataifNOTEmpty('price', $price);
$AddtoDataifNOTEmpty('owneruid', $owneruid);
$AddtoDataifNOTEmpty('createdby', $createdby);
$AddtoDataifNOTEmpty('name', $name);
$AddtoDataifNOTEmpty('description', $description);
$AddtoDataifNOTEmpty('description', $remarks);
$AddtoDataifNOTEmpty('transactiondata', $transactiondata);
$AddtoDataifNOTEmpty('transactiondata', $logs);
$AddtoDataifNOTEmpty('created', $created);
$AddtoDataifNOTEmpty('modified', $modified);
return $this->InsertIntoDB($data);
}
function getTransactionsSessions()
{
}
}
class DB_PRODUCTS_TRANSACTIONS_QUICK_MULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'products_transactions';
private $parentidresults = [];
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
}
function DB_PRODUCTS_TRANSACTIONS($DB = false)
{
return new DB_PRODUCTS_TRANSACTIONS($DB);
}
function DB_PRODUCTS_TRANSACTIONS_QUICK_MULTIPLESEARCH($DB = false)
{
return new DB_PRODUCTS_TRANSACTIONS_QUICK_MULTIPLESEARCH($DB);
}

View File

@@ -0,0 +1,143 @@
<?php
class DB_PRODUCTS_TRANSACTIONS_SESSIONS
{
public $DB = false;
public $tablename = 'products_transactions_sessions';
use BASICDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
function NewProductsTransactionSession($storeuid, $status = 'open', $name = '', $description = '', $category = '', $subtype = '', $createdby = '', $createdfor = '', $remarks = '')
{
if (!$storeuid) {
return false;
}
$store = DB_STORES()->getDetailsbyUIDorHashkey($storeuid);
if (!$storeuid) {
return false;
}
$store_owneruid = $store['owneruid'];
$store_manageruid = $store['manageruid'];
$date = serverdatetimesql();
$created = $date;
$modified = $date;
global $CurrentUserUID;
if (!$createdby) {
$createdby = $CurrentUserUID;
}
if (!$createdby) {
$createdby = CurrentUserUID();
}
if (!$createdfor) {
$createdfor = $createdby;
}
$createdby_details = DBQUERY()->USERS()->getDetailsbyUIDorHashkey($createdby);
if (!$createdby_details) {
return false;
}
if ($createdby !== $createdfor) {
$createdfor_details = DBQUERY()->USERS()->getDetailsbyUIDorHashkey($createdfor);
} else {
$createdfor_details = $createdby_details;
}
$ownerdetails = DBQUERY()->USERS()->getDetailsbyUIDorHashkey($store_owneruid);
if (!$ownerdetails) {
return false;
}
if (!MARKET_MAIN()->isNewTransactionAllowed($createdby_details, $createdfor_details, $ownerdetails)) {
return false;
}
$logs = [$date, 'Created New Transaction by uid(' . $createdby . ')'];
$hashkey = $this->GenerateNewHash();
$data = [
'hashkey' => $hashkey,
'created' => $date,
'modified' => $date,
'name' => $name,
'description' => $description,
'category' => $category,
'subtype' => $subtype,
'createdby' => $createdby,
'createdfor' => $createdfor,
'remarks' => $remarks,
'logs' => $logs,
'status' => $status
];
foreach ($data as $key => $value) {
if (!$value) {
unset($data[$key]);
}
if (is_array($value)) {
$data[$key] = tryjsonencode($value);
}
}
return $insert = $this->InsertIntoDB($data);
}
}
class DB_PRODUCTS_TRANSACTIONS_SESSIONS_QUICK_MULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'products_transactions_sessions';
private $parentidresults = [];
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
}
function DB_PRODUCTS_TRANSACTIONS_SESSIONS($DB = false)
{
return new DB_PRODUCTS_TRANSACTIONS_SESSIONS($DB);
}
function DB_PRODUCTS_TRANSACTIONS_SESSIONS_QUICK_MULTIPLESEARCH($DB = false)
{
return new DB_PRODUCTS_TRANSACTIONS_SESSIONS_QUICK_MULTIPLESEARCH($DB);
}

View File

@@ -0,0 +1,448 @@
<?php
class DB_PRODUCTS
{
public $DB = false;
public $tablename = 'products';
use BASICDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
function NewWholesaleProduct(
$name,
$description,
$price,
$unitname,
$storeuid,
$available = 0,
$photourl = '',
$barcode = '',
$specs = '',
$owneruid = '',
$sold = '',
$category = '',
$subcategory = '',
$remarks = '',
$status = '',
$sku = '',
$shortcode = '',
$shortname = '',
$qrcode = '',
$reviews = '',
$createdby = '',
$rating = '',
$createdfor = ''
) {
if (!$specs) {
$specs = [];
}
$specs[]='wholesale';
return $this->NewProduct($name,
$description,
$price,
$unitname,
$storeuid,
$available,
$photourl,
$barcode,
$specs,
$owneruid ,
$sold,
$category,
$subcategory,
$remarks,
$status,
$sku,
$shortcode,
$shortname,
$qrcode,
$reviews,
$createdby,
$rating,
$createdfor);
}
function NewProduct(
$name,
$description,
$price,
$unitname,
$storeuid,
$available = 0,
$photourl = '',
$barcode = '',
$specs = '',
$owneruid = '',
$sold = '',
$category = '',
$subcategory = '',
$remarks = '',
$status = '',
$sku = '',
$shortcode = '',
$shortname = '',
$qrcode = '',
$reviews = '',
$createdby = '',
$rating = '',
$createdfor = ''
) {
if (!$name || !$description || !$price || !$unitname || !$storeuid) {
return false;
}
$storedata = DBQUERY()->STORES()->CheckifUIDorHashKeyExist($storeuid);
if (!$storedata) {
return false;
}
$data = [];
$addtofinalDBVar = function ($varvalue, $varname, $tryjson = false) use (&$data) {
if ($varvalue) {
if ($tryjson and is_array($varvalue)) {
$varvalue = tryjsonencode($varvalue);
}
$data[$varname] = $varvalue;
}
};
global $CurrentUserUID;
if (!$createdby) {
$createdby = $CurrentUserUID;
}
if (!$createdby) {
$createdby = CurrentUserUID();
}
if (!$createdfor) {
$createdfor = $createdby;
}
if (!$status) {
$status = 'active';
} else {
$status = strtolower($status);
}
$ownerUIDCheckifParent = function ($owneruid, $createdfor, $createdby) {
$createdby_isParentofOwner = DB_USERS()->isTargetUserAChildofParent($owneruid, $createdby);
if ($createdby_isParentofOwner) {
return true;
}
$createdfor_isParentofOwner = DB_USERS()->isTargetUserAChildofParent($owneruid, $createdfor);
if ($createdfor_isParentofOwner) {
return true;
}
$Owner_isParentofCreatedby = DB_USERS()->isTargetUserAChildofParent($createdby, $owneruid);
if ($Owner_isParentofCreatedby) {
return true;
}
return false;
};
$createdby_details = DB_USERS()->getDetailsbyUIDorHashkey($createdby);
if (!$createdby_details) {
return false;
}
if ($createdby !== $createdfor) {
$createdfor_details = DB_USERS()->getDetailsbyUIDorHashkey($createdfor);
} else {
$createdfor_details = $createdby_details;
}
if (!$createdfor_details) {
return false;
}
$createdby_type = $createdby_details['acct_type'];
$createdby_uid = $createdby_details['uid'];
$createdbyfor_type = $createdfor_details['acct_type'];
$createdfor_uid = $createdfor_details['uid'];
$storeuid_details = $storedata;
if (!$storeuid) {
return false;
}
$storeuid = $storeuid_details['uid'];
$storeuid_owneruid = $storeuid_details['owneruid'];
if (!$owneruid) {
$owneruid = $storeuid_owneruid;
}
$createdby_is_StoreOwner = $createdby_type === 'storeowner';
$createdby_is_StoreManager = $createdby_type === 'storemanager';
$createdfor_is_StoreOwner = $createdbyfor_type === 'storeowner';
$createdfor_is_StoreManger = $createdbyfor_type === 'storemanager';
$createdby_is_Ult = $createdby_type === 'ult';
$createdby_is_Operator = $createdby_type === 'operator';
$createdby_is_Admin = $createdby_type === 'admincore';
if (!$createdby_is_StoreManager && !$createdby_is_StoreOwner && !$createdfor_is_StoreOwner && !$createdfor_is_StoreManger && !$createdby_is_Ult && !$createdby_is_Operator && !$createdby_is_Admin) {
return false;
}
if ($owneruid) {
$owner_details = DBQUERY()->USERS()->getDetailsbyUIDorHashkey($owneruid);
if (!$owner_details) {
return false;
}
}
if (!$ownerUIDCheckifParent($owneruid, $createdfor, $createdby)) {
return false;
}
$createdby_is_SameUIDAsStoreOwner = $createdby_uid === $storeuid_owneruid;
$createdfor_is_SameUIDAsStoreOwner = $createdfor_uid === $storeuid_owneruid;
$isOwnerofStore = $createdby_is_SameUIDAsStoreOwner || $createdfor_is_SameUIDAsStoreOwner || $createdby_is_Ult;
if (!$isOwnerofStore) {
return false;
}
$userdata = DB_USERS()->GetUserDatabyUID($createdby, ['username', 'nickname']);
if (!$userdata) {
return false;
}
$username = $userdata['username'] ?? 'unknown';
$created = serverdatetimesql();
$logs = [serverdatetimesql(), 'Product Created On ' . serverdatetimesql() . ' by useruid(' . $createdby . ')'];
$hash = generatenewhash($this->tablename);
$addtofinalDBVar($name, 'name');
$addtofinalDBVar($description, 'description');
$addtofinalDBVar($price, 'price');
$addtofinalDBVar($unitname, 'unitname');
$addtofinalDBVar($storeuid, 'storeuid');
$addtofinalDBVar($available, 'available');
$addtofinalDBVar($photourl, 'photourl', true);
$addtofinalDBVar($barcode, 'barcode');
$addtofinalDBVar($specs, 'specs', 1);
$addtofinalDBVar($owneruid, 'owneruid');
$addtofinalDBVar($sold, 'sold');
$addtofinalDBVar($category, 'category');
$addtofinalDBVar($subcategory, 'subcategory');
$addtofinalDBVar($remarks, 'remarks');
$addtofinalDBVar($status, 'status');
$addtofinalDBVar($sku, 'sku');
$addtofinalDBVar($shortcode, 'shortcode');
$addtofinalDBVar($shortname, 'shortname');
$addtofinalDBVar($qrcode, 'qrcode');
$addtofinalDBVar($rating, 'rating');
$addtofinalDBVar($reviews, 'reviews', 1);
$addtofinalDBVar($createdby, 'createdby');
$addtofinalDBVar($createdfor, 'createdfor');
$addtofinalDBVar($logs, 'logs');
$addtofinalDBVar($hash, 'hashkey');
$addtofinalDBVar($created, 'created');
global $DB;
if (!$DB) {
$DB = DB();
}
$key = insertintodb($DB, 'products', $data);
if ($key) {
DBQUERY()->PRODUCTS_HISTORY()->NewProductHistory($data);
}
return $key;
}
function ShowProductsbyStoreUID($uidorhashkey, $onlyactive = true, $fieldstoselect = [])
{
if (!$uidorhashkey) {
return false;
}
if (!is_numeric($uidorhashkey)) {
$uidorhashkey = DB_STORES()->getUIDfromHashkey($uidorhashkey) ?? false;
if (!$uidorhashkey) {
return false;
}
}
$data = ['storeuid' => $uidorhashkey];
if ($onlyactive) {
$data['status'] = 'active';
}
return $data = $this->ListFromDB($data, [], $fieldstoselect);
}
function getReviewsbyUID($uidorhashkey)
{
if (!$uidorhashkey) {
return false;
}
$productdetails = $this->getDetailsbyUIDorHashkey($uidorhashkey);
if (!$productdetails) {
return false;
}
return $reviews = tryjsondecode($productdetails['reviews']) ?? '';
}
function setReviewsbyUID($uidorhashkey, $reviews)
{
if (!$uidorhashkey) {
return false;
}
if ($reviews) {
$reviews = tryjsonencode($reviews);
}
return $this->ModifyDBSinglefieldbyUID($uidorhashkey, 'reviews', $reviews);
}
function addReviewbyUID($uidorhashkey, $useruid, $rating, $content)
{
if (!$uidorhashkey || !$useruid || !$rating || !$content) {
return false;
}
$reviews = $this->getReviewsbyUID($uidorhashkey);
if ($reviews === false) {
return false;
}
$lastelement = $reviews[count($reviews) - 1] ?? false;
if (!$lastelement) {
$count = $lastelement[0] ?? 1;
}
$createddate = serverdatetimesql();
$modifieddate = serverdatetimesql();
$userdetails = DBQUERY()->USERS()->GetUserDatabyUID($useruid);
if (!$userdetails) {
return false;
}
$userSUID = $userdetails['uid'];
$userHash = $userdetails['hashkey'];
$reviews[] = [$count, $createddate, $modifieddate, $userHash, $rating, $content];
return $this->setReviewsbyUID($uidorhashkey, $reviews);
}
function clearReviewsbyUID($uidorhashkey)
{
return $this->setReviewsbyUID($uidorhashkey, []);
}
function listProductsbyStatus($status = null, $fieldstoselectarray = '', )
{
return DBQUERY()->PRODUCTS()->ListFromDB(['status' => strtolower($status)], $likefields = [], $fieldstoselectarray) ?? false;
}
function listActiveProductsStatus($fieldstoselectarray = '')
{
return $this->listProductsbyStatus('active', $fieldstoselectarray);
}
function listProductsbyStatuswithStock($status = null, $fieldstoselectarray = '')
{
$noavailableflag = 0;
if ($fieldstoselectarray && !in_array('available', $fieldstoselectarray)) {
$noavailableflag = 1;
$fieldstoselectarray[] = 'available';
}
$list = $this->listProductsbyStatus($status, $fieldstoselectarray);
if (!$list) {
return false;
}
foreach ($list as $key => $value) {
if ($value['available'] < 1) {
unset($list[$key]);
continue;
}
if ($noavailableflag) {
unset($list[$key]['available']);
}
}
$list = array_values($list);
return $list;
}
function listAvailableandActiveProducts($fieldstoselectarray = '')
{
return $this->listProductsbyStatuswithStock('active', $fieldstoselectarray);
}
}
class DB_PRODUCTS_QUICK_MULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'products';
private $parentidresults = [];
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
}
function DB_PRODUCTS_QUICK_MULTIPLESEARCH($DB = false)
{
return new DB_PRODUCTS_QUICK_MULTIPLESEARCH($DB);
}
function DB_PRODUCTS($DB = false)
{
return new DB_PRODUCTS($DB);
}

View File

@@ -0,0 +1,62 @@
<?php
class DB_PRODUCTSHISTORY
{
public $DB = false;
public $tablename = 'productshistory';
use BASICDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
function NewProductHistory($data)
{
$required = ['available', 'price', 'name', 'description', 'status', 'storeuid'];
return $this->DefaultDBInsert($data, $required);
}
}
class DB_PRODUCTSHISTORY_QUICK_MULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'productshistory';
private $parentidresults = [];
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
}
function DB_PRODUCTSHISTORY($DB = false)
{
return new DB_PRODUCTSHISTORY($DB);
}
function DB_PRODUCTSHISTORY_QUICK_MULTIPLESEARCH($DB = false)
{
return new DB_PRODUCTSHISTORY_QUICK_MULTIPLESEARCH($DB);
}

View File

@@ -0,0 +1,196 @@
<?php
class DB_STORES
{
public $DB = false;
public $tablename = 'stores';
use BASICDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
public function GenerateStoreCode($category, $num=null, $length = 8, )
{
$characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_';
$charactersLength = strlen($characters);
$storeCode = '';
if(!$num){
$num = rand(1,99);
}
// Generate a random alphanumeric string
for ($i = 0; $i < $length; $i++) {
$storeCode .= $characters[rand(0, $charactersLength - 1)];
}
$categoryPrefix = substr($category, 0, 2);
$categorySuffix = substr($category, -2, 2);
$cat = $categoryPrefix . $categorySuffix;
$storeCode = date('Y') . '-' . $cat . '-' . $num . '-' . $storeCode;
return $storeCode;
}
private function generateStoreCodeUnique($storeCodeArray, $category, $num = '', $length = 8)
{
$storeCodeUnique = false;
while (!$storeCodeUnique) {
$newstorecode = $this->GenerateStoreCode($category, $num, $length = 8);
if (!in_array(strtoupper($storeCodeUnique), $storeCodeArray)) {
$storeCodeUnique = $newstorecode;
break;
}
}
return $newstorecode;
}
function generateNewStoreCodefromDB($category, $num = '', $length = 8)
{
$storecodes = $this->ListFromDB([], [], ['storecode']);
if (!$storecodes) {
$storecodes = [];
}
$storecodes = array_column($storecodes, 'storecode');
$targetdateYEARMONTH = date('Y') . '-' . date('m');
$datecreated = $this->ListbyDateCreated($targetdateYEARMONTH);
$newnum = (count($datecreated)) + 1;
if (!$num) {
$num = $targetdateYEARMONTH . '-' . $newnum;
}
$uniqueStoreCode = $this->generateStoreCodeUnique($storecodes, $category, $num, $length);
return $uniqueStoreCode;
}
function NewStore($name, $description, $owneruid, $manageruid, $storeAddress, $category = '', $subcategory = '', $photourl = '', $storecode = '', $status = 'active', $specs = '', $additionaldata = '', $remarks = '', $createdby = '')
{
if (!$name || !$description || !$owneruid || !$manageruid || !$storeAddress) {
return false;
}
$GetUserDetails = function ($useruid) {
if (!$useruid) {
return false;
}
$details = DBQUERY()->USERS()->GetUserDatabyUID($useruid);
return $details;
};
$addtoDataifNotEmpty = function ($varname, $varvalue) use (&$data) {
if (!$varname || !$varvalue) {
return false;
}
if (is_array($varvalue)) {
$varvalue = tryjsonencode($varvalue);
}
$data[$varname] = $varvalue;
};
global $CurrentUserUID;
if (!$createdby) {
$createdby = $CurrentUserUID;
}
if (!$createdby) {
$createdby = CurrentUserUID();
}
$createdby_details = DB_USERS()->getDetailsbyUIDorHashkey($createdby) ?? false;
if (!$createdby_details) {
return false;
}
$owner_details = $GetUserDetails($owneruid);
if (!$owner_details) {
return false;
}
$manager_details = $GetUserDetails($manageruid);
if (!$manager_details) {
return false;
}
if (!MARKET_MAIN()->isNewTransactionAllowed($createdby_details, $manager_details, $owner_details)) {
return false;
}
if (!$storecode) {
$storecode = $this->generateNewStoreCodefromDB($category);
}
$datetimenow = serverdatetimesql();
$hashkey = $this->GenerateNewHash();
$logs = [$datetimenow, 'Created New Store by useruid(' . $createdby . ') at ' . $datetimenow];
$data = [
'hashkey' => $hashkey,
'created' => $datetimenow,
'modified' => $datetimenow,
'logs' => $logs,
'name' => $name,
'description' => $description,
'owneruid' => $owneruid,
'manageruid' => $manageruid,
'createdby' => $createdby,
'address' => $storeAddress
];
$addtoDataifNotEmpty('category', $category);
$addtoDataifNotEmpty('subcategory', $subcategory);
$addtoDataifNotEmpty('photourl', $photourl);
$addtoDataifNotEmpty('storecode', $storecode);
$addtoDataifNotEmpty('status', $status);
$addtoDataifNotEmpty('specs', $specs);
$addtoDataifNotEmpty('additionaldata', $additionaldata);
$addtoDataifNotEmpty('remarks', $remarks);
$insert = $this->InsertIntoDB($data);
return $insert;
}
}
class DB_STORES_QUICK_MULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'stores';
private $parentidresults = [];
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
}
function DB_STORES($DB = false)
{
return new DB_STORES($DB);
}
function DB_STORES_QUICK_MULTIPLESEARCH($DB = false)
{
return new DB_STORES_QUICK_MULTIPLESEARCH($DB);
}

View File

@@ -0,0 +1,75 @@
<?php
class DB_SUPPLIERS
{
public $DB = false;
public $tablename = 'suppliers';
use BASICDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
function NewSupplier()
{
$data = [
];
return $this->InsertBasicDBHashCreatedModified($data);
}
}
class DB_SUPPLIERS_QUICK_MULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'suppliers';
private $parentidresults = [];
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
}
function DB_SUPPLIERS($DB = false)
{
return new DB_SUPPLIERS($DB);
}
function DB_SUPPLIERS_QUICK_MULTIPLESEARCH($DB = false)
{
return new DB_SUPPLIERS_QUICK_MULTIPLESEARCH($DB);
}

View File

@@ -0,0 +1,429 @@
<?php
$classMap['PROPERTIES'] = 'DB_PROPERTIES';
$classMap['REFERRALS'] = 'DB_REFERRALS';
$classMap['REFERRAL_KEYS'] = 'DB_REFERRAL_KEYS';
class DB_REFERRALS
{
public $DB = false;
public $tablename = 'referrals';
public $logs;
public $status;
use BASICDB;
use LOGSDB;
use STATUSDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
$this->logs = new stdClass();
$this->logs->setLogbyUID = [$this, 'setLogbyUID'];
$this->logs->deleteFullLogbyUID = [$this, 'deleteFullLogbyUID'];
$this->logs->viewLogsbyUID = [$this, 'viewLogsbyUID'];
$this->logs->deleteLogbyArrayIndex = [$this, 'deleteLogbyArrayIndex'];
$this->logs->AddLog = [$this, 'AddLog'];
$this->status = new stdClass();
$this->status->UpdateStatus = [$this, 'UpdateStatus'];
$this->status->ViewStatus = [$this, 'ViewStatus'];
$this->status->SetStatusAsDenied = [$this, 'SetStatusAsDenied'];
$this->status->SetStatusAsOngoing = [$this, 'SetStatusAsOngoing'];
$this->status->SetStatusAsNULL = [$this, 'SetStatusAsNULL'];
$this->status->SetStatusAsNULL = [$this, 'SetStatusAsApproved'];
}
function NewReferral($fullname, $mobile, $landline = '', $facebookurl = '', $email = '', $property_location = '', $target_property = '', $target_viewing_date = '', $firstname = '', $middlename = '', $lastname = '', $referral_key = 0, $remarks = '', $alt_contact_details = '', $status = 0)
{
if (!$fullname || !$mobile) {
return false;
}
if (!$referral_key) {
$referral_key = CurrentUserUID();
}
if (!$referral_key) {
$referral_key = '';
}
$target_property = $target_property ?? 0;
$hash = $this->GenerateNewHash();
if (!$alt_contact_details) {
$alt_contact_details = '[]';
} elseif (is_array($alt_contact_details)) {
$alt_contact_details = json_encode($alt_contact_details);
}
$data = [];
$ConvertToJSONifPossible = function ($datakey, $dataarraay) use (&$data) {
if (!$datakey) {
return;
}
if ($dataarraay) {
if (is_array($dataarraay)) {
$data[$datakey] = json_encode($dataarraay);
} else {
$data[$datakey] = $dataarraay;
}
} else {
$data[$datakey] = '[]';
}
};
$ifNotNullAddtoArray = function ($datakey, $dataval) use (&$data) {
if (!$dataval) {
return;
}
$data[$datakey] = $dataval;
};
// file_put_contents('results.txt', print_r($email, 1) . print_r($landline, 1));
//fix target_property in DB
$data = [
'hashkey' => $hash,
'fullname' => $fullname,
'mobile' => $mobile,
'referral_key' => $referral_key,
'email' => $email ?? '',
];
if ($target_property && !is_numeric($target_property)) {
$target_property = DBQUERY($this->DB)->PROPERTIES()->getUIDfromHashkey($target_property) ?? 0;
}
$ifNotNullAddtoArray('referral_key', $referral_key);
$ifNotNullAddtoArray('landline', $landline);
$ifNotNullAddtoArray('target_property', $target_property);
$ifNotNullAddtoArray('property_location', $property_location);
$ifNotNullAddtoArray('facebookurl', $facebookurl);
$ifNotNullAddtoArray('firstname', $firstname);
$ifNotNullAddtoArray('middlename', $middlename);
$ifNotNullAddtoArray('lastname', $lastname);
$ifNotNullAddtoArray('remarks', $remarks);
$ifNotNullAddtoArray('target_viewing_date', $target_viewing_date);
$ConvertToJSONifPossible('alt_contact_details', $alt_contact_details);
if ($target_viewing_date) {
$data['target_viewing_date'] = $target_viewing_date;
}
return $this->InsertIntoDB($data);
}
public function SetStatusAsDenied($uidorhashkey)
{
return $this->status->UpdateStatus($uidorhashkey, -1);
}
public function SetStatusAsOngoing($uidorhashkey)
{
return $this->status->UpdateStatus($uidorhashkey, 1);
}
public function SetStatusAsNULL($uidorhashkey)
{
return $this->status->UpdateStatus($uidorhashkey, 0);
}
public function SetStatusAsApproved($uidorhashkey)
{
return $this->status->UpdateStatus($uidorhashkey, 2);
}
}
function DB_REFERRALS($DB = false)
{
return new DB_REFERRALS($DB);
}
class REFERRALS_QUICKMULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'referrals';
private $parentidresults = [];
public $DB;
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata);
}
function FindbyExactFullName($name, $newdata = false)
{
return $this->Find('fullname', $name, $exact = true, true, $newdata)['name'];
}
function ListbySimilarNames($name, $newdata = false, $usestrpos = false)
{
return $this->List('fullname', $name, false, true, $newdata, $usestrpos);
}
}
function REFERRALS_QUICKMULTIPLESEARCH($DB = false)
{
return new REFERRALS_QUICKMULTIPLESEARCH($DB);
}
class DB_PROPERTIES
{
public $DB = false;
public $tablename = 'properties';
use BASICDB;
use LOGSDB;
use STATUSDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
function NewProperties($name, $description, $location, $photourl, $sqm = '', $rooms = NULL, $bedrooms = '', $kitchen = '', $toilet = '', $floors = '', $specs = '', $category = '', $subcategory = '', $status = 0, $remarks = '', $forbiddenUIDsArray = '', $createdby = '', $price = '')
{
if (!$name || !$description || !$location || !$photourl) {
return false;
}
if ($forbiddenUIDsArray and is_array($forbiddenUIDsArray)) {
$forbiddenUIDsArray = json_encode($forbiddenUIDsArray);
}
$data = [];
$ConvertToJSONifPossible = function ($datakey, $dataarraay) use (&$data) {
if (!$datakey) {
return;
}
if ($dataarraay) {
if (is_array($dataarraay)) {
$data[$datakey] = json_encode($dataarraay);
} else {
$data[$datakey] = $dataarraay;
}
} else {
$data[$datakey] = '[]';
}
};
$ifNotNullAddtoArray = function ($datakey, $dataval) use (&$data) {
if (!$dataval) {
return;
}
$data[$datakey] = $dataval;
};
$hashkey = $this->GenerateNewHash();
if (!$createdby) {
$createdby = CurrentUserUID();
}
if ($createdby) {
$data['createdby'] = $createdby;
}
$data['hashkey'] = $hashkey;
$data['name'] = $name;
$data['description'] = $description;
$data['location'] = $location;
$ConvertToJSONifPossible('photourl', $photourl);
$ifNotNullAddtoArray('sqm', $sqm);
$ifNotNullAddtoArray('rooms', $rooms);
$ifNotNullAddtoArray('bedrooms', $bedrooms);
$ifNotNullAddtoArray('kitchen', $kitchen);
$ifNotNullAddtoArray('toilet', $toilet);
$ifNotNullAddtoArray('floors', $floors);
$ifNotNullAddtoArray('category', $category);
$ifNotNullAddtoArray('subcategory', $subcategory);
$ifNotNullAddtoArray('price', $price);
$ConvertToJSONifPossible('specs', $specs);
$data['status'] = $status ?? 0;
$data['remarks'] = $remarks ?? '';
$data['forbiddenuids'] = $forbiddenUIDsArray ?? '';
$data['logs'] = '[["' . serverdatetimesql() . '","Created Property"]]';
$Insert = $this->InsertIntoDB($data);
return $Insert;
}
}
function DB_PROPERTIES($DB = false)
{
return new DB_PROPERTIES($DB);
}
class PROPERTIES_QUICKMULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'properties';
private $parentidresults = [];
public $DB;
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
}
function PROPERTIES_QUICKMULTIPLESEARCH($DB = false)
{
return new PROPERTIES_QUICKMULTIPLESEARCH($DB);
}
class DB_REFERRAL_KEYS
{
public $DB = false;
public $tablename = 'referral_keys';
use BASICDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
function NewReferralKey($useruid = '', $propertyuid = 0, $referral_code = 0)
{
if (!$useruid) {
$useruid = CurrentUserUID();
}
if (!$useruid) {
$useruid = '';
}
if ($useruid and is_numeric($useruid)) {
$useruid = DB_USERS($this->DB)->GetUserDatabyUID($useruid)['uid'] ?? '';
}
$hashkey = $this->GenerateNewHash();
$data['hashkey'] = $hashkey;
$data['useruid'] = $useruid ?? 0;
$data['propertyuid'] = $propertyuid ?? 0;
if ($referral_code) {
$referral_code_exists = $this->getDetailsbyReferral_Key($referral_code);
if ($referral_code_exists) {
return false;
}
$data['referral_code'] = $referral_code;
} else {
$data['referral_code'] = generateUniqueReferralCode($this->DB, 'referral_code', $this->tablename);
}
$Insert = $this->InsertIntoDB($data);
return $Insert;
}
function getDetailsbyReferral_Key($referral_code, $fieldstoselect = '')
{
if (!$referral_code) {
return false;
}
$details = $this->ListFromDB(['referral_code' => $referral_code], [], $fieldstoselect);
return $details[0] ?? false;
}
}
function DB_REFERRAL_KEYS($DB = false)
{
return new DB_REFERRAL_KEYS($DB);
}
class REFERRAL_KEYS_QUICKMULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'referral_keys';
private $parentidresults = [];
public $DB;
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false, $DB = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata, $DB);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
function GetReferralCodebyUID($uidorhashkey, $newdata = false)
{
return $this->getValueByUIDorHashkey($uidorhashkey, 'referral_code', $newdata);
}
}
function REFERRAL_KEYS_QUICKMULTIPLESEARCH($DB = false)
{
return new REFERRAL_KEYS_QUICKMULTIPLESEARCH($DB);
}
?>

View File

@@ -0,0 +1,62 @@
<?php
class DB_TABLENAME
{
public $DB = false;
public $tablename = 'users';
use BASICDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
function NewRecord()
{
}
}
class DB_TABLENAME_QUICK_MULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'tablename';
private $parentidresults = [];
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
}
function DB_TABLENAME($DB=false){
return new DB_TABLENAME($DB);
}
function DB_TABLENAME_QUICK_MULTIPLESEARCH($DB=false){
return new DB_TABLENAME_QUICK_MULTIPLESEARCH($DB);
}
?>

View File

@@ -0,0 +1,306 @@
<?php
class DB_USERINFO
{
public $DB = false;
public $tablename = 'userinfo';
use BASICDB;
public function __construct($DB = false)
{
if (!$DB) {
$DB = DB();
}
if (!$DB) {
return false;
}
if ($DB) {
$this->DB = $DB;
}
if (!$this->tablename) {
return false;
}
}
function NewUserInfo($targetuserlogin, $fullname = '', $firstname = '', $middlename = '', $lastname = '', $landline = '', $mobile = '', $email = '', $altemail = '', $altlandline = '', $altmobile = '', $credit_card = '', $bankdetails = '[]', $facebookurl = '', $photourl = '[]', $otherdetails = '[]', $addresses = '[]')
{
if (!$targetuserlogin) {
return false;
}
$usercheck = DBQUERY()->USERS()->getDetailsbyUIDorHashkey($targetuserlogin)['uid'] ?? false;
if (!$usercheck) {
return false;
}
$hashkey = $this->GenerateNewHash();
if ($bankdetails) {
if (is_array($bankdetails)) {
$bankdetails = json_encode($bankdetails);
}
} else {
$bankdetails = '[]';
}
if ($otherdetails) {
if (is_array($otherdetails)) {
$otherdetails = json_encode($otherdetails);
}
} else {
$otherdetails = '[]';
}
if ($addresses) {
if (is_array($otherdetails)) {
$otherdetails = json_encode($otherdetails);
}
} else {
$addresses = '[]';
}
$data = [
'hashkey' => $hashkey,
'targetuserlogin' => $targetuserlogin,
'fullname' => $fullname,
'firstname' => $firstname,
'middlename' => $middlename,
'lastname' => $lastname,
'landline' => $landline,
'mobile' => $mobile,
'email' => $email,
'altemail' => $altemail,
'altlandline' => $altlandline,
'altmobile' => $altmobile,
'credit_card' => $credit_card,
'bankdetails' => $bankdetails,
'facebookurl' => $facebookurl,
'photourl' => $photourl,
'otherdetails' => $otherdetails,
'addresses' => $addresses
];
$insert = $this->InsertIntoDB($data);
return $insert;
}
function GetbyTargetUserUID($useruidorhashkey, $fieldstoselect = '')
{
$result = $this->ListbyTargetUserUID($useruidorhashkey, $fieldstoselect);
return $result[0] ?? [];
}
function ListbyTargetUserUID($useruidorhashkey, $fieldstoselect = '')
{
if (!$useruidorhashkey) {
return false;
}
if (!is_numeric($useruidorhashkey)) {
$useruidorhashkey = DB_USERS($this->DB)->GetUserUIDbyHashkey($useruidorhashkey) ?? false;
}
if (!$useruidorhashkey || !is_numeric($useruidorhashkey)) {
return false;
}
return $this->ListFromDB(['targetuserlogin' => $useruidorhashkey], [], $fieldstoselect);
}
function ModifybyTargetUserUID($targetuserUID, $data)
{
if (!$targetuserUID || !$data) {
return false;
}
$wherearray = ['targetuserlogin' => $targetuserUID];
return $this->ModifySingleRowwithVerification($data, $wherearray);
}
function GetAddresses($targetuserUID)
{
if (!$targetuserUID) {
return false;
}
$details = $this->GetbyTargetUserUID($targetuserUID);
if (!$details) {
return false;
}
$addresses = $details['addresses'] ?? null;
if ($addresses) {
$addresses = tryjsondecode($addresses, true);
}
return $addresses;
}
function GetLastAddress($targetuserUID)
{
$details = $this->GetAddresses($targetuserUID);
if ($details === false) {
return false;
}
if ($details === null) {
return null;
}
$details[count($details) - 1]['key'] = count($details) - 1;
return $details[count($details) - 1];
}
function getDefaultAddress($targetuserUID)
{
if (!$targetuserUID) {
return false;
}
$details = $this->GetAddresses($targetuserUID);
if (!$details) {
return false;
}
if (count($details) === 1) {
$details['key'] = 0;
return $details[0];
}
foreach ($details as $key => $value) {
if ($value['default']) {
$value['key'] = $key;
return $value;
}
}
return null;
}
function SetAddresses($targetuserUID, $newaddresses)
{
if (!$targetuserUID || !is_numeric($targetuserUID)) {
return false;
}
$data = [];
if (is_array($newaddresses)) {
$newaddresses = array_values($newaddresses);
$newaddresses = json_encode($newaddresses);
}
$data['addresses'] = $newaddresses;
return $this->ModifybyTargetUserUID($targetuserUID, $data);
}
function AddAddress($targetuserUID, $address, $addressname, $contactname, $contactnumber, $default = false)
{
if (!$targetuserUID || !$address || !$addressname || !$contactname || !$contactnumber) {
return false;
}
if ($default === 'true') {
$default = true;
} elseif ($default === 'false') {
$default = 0;
}
$data = $this->GetAddresses($targetuserUID);
if ($data === false) {
return false;
}
if (!$data) {
$data = [];
}
if ($default) {
foreach ($data as $key => $value) {
$data[$key][4] = false;
}
}
$data[] = [$address, $addressname, $contactname, $contactnumber, $default];
$submit = $this->SetAddresses($targetuserUID, $data);
if (!$submit) {
return false;
}
return $this->GetLastAddress($targetuserUID);
}
function RemoveAddress($targetuserUID, $index)
{
if (!$targetuserUID || !is_numeric($index)) {
return false;
}
$addressees = $this->GetAddresses($targetuserUID);
if ($addressees === false) {
return false;
}
if (!isset($addressees[$index])) {
return false;
}
unset($addressees[$index]);
$addressees = array_values($addressees);
return $this->SetAddresses($targetuserUID, $addressees);
}
function ModifyAddressbyIndex($targetuserUID, $index,$name=null,$address=null,$contactname=null,$contactnumber=null,$default=null)
{
if(!$name && !$address && !$contactname && !$contactnumber && $default===null) {return false;}
if (!$targetuserUID || !is_numeric($index)) {
return false;
}
$addressees = $this->GetAddresses($targetuserUID);
if ($addressees === false) {
return false;
}
if (!isset($addressees[$index])) {
return false;
}
$dataname= $addressees[$index][1] ?? false;
$dataaddress = $addressees[$index][0] ?? false;
$datacontactname =$addressees[$index][2] ?? false;
$datacontactnumber = $addressees[$index][3] ?? false;
$datadefault = $addressees[$index][4] ?? false;
$finalname = $name ?? $dataname;
$finaladdress = $address ?? $dataaddress;
$finalcontactname = $contactname ?? $datacontactname;
$finaldatacontactnumber =$contactnumber ?? $datacontactnumber;
if($default !==null){
$finaldefault = $default;
}else{
$finaldefault= $datadefault;
}
if ($finaldefault) {
foreach ($addressees as $key => $value) {
$addressees[$key][4] = false;
}
}
$addressees[$index]=[$finaladdress,$finalname,$finalcontactname,$finaldatacontactnumber,$finaldefault];
$addressees = array_values($addressees);
return $this->SetAddresses($targetuserUID, $addressees);
}
function SetAddressAsDefault($targetuserUID,$index){
return $this->ModifyAddressbyIndex($targetuserUID, $index,null,null,null,null,true);
}
function EmptyAddresses($targetuserUID)
{
return $this->SetAddresses($targetuserUID, '');
}
function ShowAddressFormat()
{
return ['Address', 'Address Name', 'Contact Name', 'Contact Number', 'default'];
}
}
function DB_USERINFO($db = false)
{
return new DB_USERINFO($db);
}
function USERINFO_MULTIPLESEARCH($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false)
{
return new USERINFO_QUICK_MULTIPLESEARCH($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata);
}
class USERINFO_QUICK_MULTIPLESEARCH
{
use DBClassSearch;
public $data;
public $tablename = 'users';
private $parentidresults = [];
public $DB;
public function __construct($data = [], $likefields = [], $fieldstoselectarray = '', $orderby = '', $noindex = 0, $whereappend = ' and ', $dateonlyarray = '', $newdata = false)
{
return $this->initialize($data, $likefields, $fieldstoselectarray, $orderby, $noindex, $whereappend, $dateonlyarray, $newdata);
}
function FindName($uid)
{
return $this->Find($fieldname = 'uid', $contenttosearch = $uid, $exact = true)[0]['name'];
}
}

View File

@@ -0,0 +1,91 @@
<?php
$altdb = 'u943309104_bukid';
$altuser = 'u943309104_bukidb';
$altpass = 'lB&3I=t6';
$althost = 'mysql';
$root_passDB = 'as73n1^1dhu';
$defaultAdminAppUser = '777';
$defaultAdminAppPassword = 'OmegaPsilon32!';
$InitializeDB = false;
$GLOBALS['defaultAdminAppUser'] = $defaultAdminAppUser;
$GLOBALS['defaultAdminAppPassword'] =$defaultAdminAppPassword;
function loadSettings()
{
$filePath = 'settings/settings.json';
if (file_exists($filePath)) {
$jsonContent = file_get_contents($filePath);
$MainSettings = json_decode($jsonContent, true);
return $MainSettings;
} else {
return null;
}
}
$MainSettings = loadSettings();
if (!file_exists('settings/DBInitialized') && $InitializeDB) {
RunDBInit();
}
function DB()
{
global $althost;
global $altdb;
global $altuser;
global $altpass;
global $defaultAdminAppUser;
global $defaultAdminAppPassword;
try {
$db = opennewdb($altdb, $althost, $altuser, $altpass);
} catch (Exception $e) {
echo $e->getMessage() . PHP_EOL;
try {
$db = opennewdb('ta');
} catch (Exception $le) {
echo 'DB Error';
exit;
}
}
return $db;
}
function RunDBInit()
{
global $althost;
global $altdb;
global $altuser;
global $altpass;
$mysqli = new mysqli($althost, $altuser, $altpass, $altdb);
echo 'Initializing DB...' . PHP_EOL;
if ($mysqli->connect_error) {
echo 'Error...' . '<br>';
die("Connection failed: " . $mysqli->connect_error);
}
echo 'Connected to DB...' . '<br>';
$query = file_get_contents("init/mysql.sql");
if ($mysqli->multi_query($query)) {
do {
} while ($mysqli->more_results() && $mysqli->next_result());
file_put_contents('settings/DBInitialized', 'true');
echo "SQL imported successfully.".'<br>' ;
} else {
echo "Error importing SQL: " . $mysqli->error.'<br>';
}
}
require_once('lib.php');
require_once('DB.php');
require_once('lib/logins.php');

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,651 @@
<?php
ini_set('session.cookie_lifetime', 315360000);
ini_set('session.gc_maxlifetime', 315360000);
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
function generatesessionhash()
{
$bytes = random_bytes(36);
return hash('sha256', bin2hex($bytes));
}
function user_access($usertype)
{
if (strtolower($usertype) === 'ult') {
$accesslist = [
'all'
];
} else if (strtolower($usertype) === 'super operator') {
$accesslist = [
'all',
'manage_users',
'manage_roles',
'manage_permissions'
];
} else if (strtolower($usertype) === 'operator') {
$accesslist = [
'view_tickets',
'create_tickets',
'edit_tickets',
'close_tickets',
'manage_assigned_tickets'
];
} else if (strtolower($usertype) === 'coordinator') {
$accesslist = [
'view_tickets',
'create_tickets',
'edit_tickets',
'close_tickets',
'manage_assigned_tickets',
'view_reports'
];
} else if (strtolower($usertype) === 'usher') {
$accesslist = [
'view_tickets',
'create_tickets',
'edit_tickets',
'close_tickets'
];
} else if (strtolower($usertype) === 'user') {
$accesslist = [
'view_tickets',
'create_tickets'
];
} else if (strtolower($usertype) === 'viewer') {
$accesslist = [
'view_tickets'
];
} else if (strtolower($usertype) === 'disabler') {
$accesslist = [];
} else {
$accesslist = [];
}
return $accesslist;
}
class WhatUserType
{
public $Usertype;
public function __construct($usertype = '---currentuser---')
{
if ($usertype === '---currentuser---') {
global $CurrentUserType;
$this->Usertype = $CurrentUserType;
} else {
$this->Usertype = $usertype;
}
}
private function ReadAndMatchType($arrayorStringUserTypes, $Targettype)
{
if (!$arrayorStringUserTypes || empty($arrayorStringUserTypes)) {
return false;
}
$types = $arrayorStringUserTypes;
if (!is_array($arrayorStringUserTypes)) {
$types = tryjsondecode($arrayorStringUserTypes);
}
if (is_array($types)) {
return in_array($Targettype, $types);
} else {
return $Targettype === $types;
}
}
public function IsUltimate()
{
return $this->ReadAndMatchType($this->Usertype, 'ult');
}
public function IsSuperOperator()
{
return $this->ReadAndMatchType($this->Usertype, 'super operator');
}
public function IsOperator()
{
return $this->ReadAndMatchType( $this->Usertype, 'operator');
}
public function IsCoordinator()
{
return $this->ReadAndMatchType( $this->Usertype, 'coordinator');
}
public function IsDisabler()
{
return $this->ReadAndMatchType( $this->Usertype, 'disabler');
}
public function IsAgent()
{
return $this->ReadAndMatchType( $this->Usertype, 'agent');
}
public function IsViewer()
{
return $this->ReadAndMatchType( $this->Usertype, 'viewer');
}
public function IsStoreManager()
{
return $this->ReadAndMatchType( $this->Usertype, 'store manager');
}
public function IsStoreOwner()
{
return $this->ReadAndMatchType( $this->Usertype, 'store owner');
}
public function IsRider()
{
return $this->ReadAndMatchType( $this->Usertype, 'rider');
}
public function IsAdminStaff()
{
return $this->ReadAndMatchType( $this->Usertype, 'admin staff');
}
public function IsTeamLeader()
{
return $this->ReadAndMatchType( $this->Usertype, 'team leader');
}
public function IsAudit()
{
return $this->ReadAndMatchType( $this->Usertype, 'audit');
}
public function IsRegionalDirector()
{
return $this->ReadAndMatchType( $this->Usertype, 'regional director');
}
public function IsRegularUser()
{
return $this->ReadAndMatchType( $this->Usertype, 'user');
}
}
function WhatUserType($usertype = '---currentuser---')
{
return new WhatUserType($usertype);
}
function SendCookieSession($sessionId, $expiration_strtotime = '')
{
if (!$sessionId) {
return false;
}
if ($expiration_strtotime) {
$expiration_strtotime = time() + 720000000;
}
setcookie('TA_SESSION_COOKIE', $sessionId, [
'expires' => $expiration_strtotime, // 30 days
'path' => '/',
'secure' => true, // Only send over HTTPS
'httponly' => false, // Accessible only by the server
'samesite' => 'Strict' // CSRF protection
]);
}
function getSessionKeyEitherCookieorSession()
{
if (!isset($_SESSION['TA']['SESSIONKEY']) or !$_SESSION['TA']['SESSIONKEY']) {
} else {
return $_SESSION['TA']['SESSIONKEY'];
}
if (isset($_COOKIE["TA_SESSION_COOKIE"]) and $_COOKIE["TA_SESSION_COOKIE"]) {
return $_COOKIE["TA_SESSION_COOKIE"];
} else {
return false;
}
}
function DeleteRemoveSessionKeyCookie()
{
setcookie("TA_SESSION_COOKIE", "", time() - 3600, "/");
$_SESSION['TA']['SESSIONKEY'] = '';
}
function UpdateSessionorCookieKey($sessionid, $expiration_strtotime = '')
{
if (!$sessionid) {
return false;
}
$_SESSION['TA']['SESSIONKEY'] = $sessionid;
SendCookieSession($sessionid, $expiration_strtotime);
}
function loginstatus()
{
if (!getSessionKeyEitherCookieorSession()) {
return false;
}
$sessiondata = getActiveSessionData(getSessionKeyEitherCookieorSession());
if (!$sessiondata) {
return false;
}
$expiry = strtotime($sessiondata['expiry']);
$now = strtotime('now');
$active = $sessiondata['userdata']['active'];
$expired = $now > $expiry;
$time_difference = $expiry - $now;
$NOT_EXPIRED = !$expired;
if ($sessiondata and $NOT_EXPIRED and $active) {
if ($time_difference < 300) {
$newexpiry = $expiry + (5 * 60);
$newexpiry = date("Y-m-d H:i:s", $newexpiry);
ModifySession($sessiondata['hashkey'], ['expiry' => $newexpiry]);
$sessiondata['expiry'] = $newexpiry;
}
if (YesNoRandom() and false) {//remove false to allow regeneration of id. currently logsuser out immediately
$newsessionid = regeneratesessionidANDUpdateSessionVar();
if ($newsessionid) {
$sessiondata['hashkey'] = $newsessionid;
}
}
UpdateSessionorCookieKey($sessiondata['hashkey'], strtotime($sessiondata['expiry']));
return $sessiondata;
} else {
deleteSession(getSessionKeyEitherCookieorSession());
return false;
}
}
function getRandomNumber($length = 1)
{
$random_bytes = openssl_random_pseudo_bytes($length);
return ord($random_bytes[0]);
}
function YesNoRandom()
{
$regenerate_threshold = 50;
$random_number = getRandomNumber(1);
if ($random_number <= $regenerate_threshold) {
return true;
} else {
return false;
}
}
function regeneratesessionid($currensessionhash)
{
if (!$currensessionhash) {
return false;
}
$newhash = generatesessionhash();
$data['hashkey'] = $newhash;
$modify = ModifySession($currensessionhash, $data);
if (!$modify) {
return false;
}
return $newhash;
}
function regeneratesessionidANDUpdateSessionVar()
{
$sessionnewid = regeneratesessionid(getSessionKeyEitherCookieorSession());
if ($sessionnewid) {
UpdateSessionorCookieKey($sessionnewid);
return $sessionnewid;
}
return false;
}
function tryloginwcookies($SESSION_ID)
{
$sessiondata = getActiveSessionData($SESSION_ID);
if (!$sessiondata) {
return false;
}
//$_SESSION['TA']['SESSIONKEY']
}
function getUserType()
{
$loginstatus = loginstatus();
if (!$loginstatus) {
return false;
}
if (isset($loginstatus['userdata']['acct_type']) and $loginstatus['userdata']['acct_type']) {
return $loginstatus['userdata']['acct_type'];
}
}
function IsUserCoordinator()
{
if (strtolower(getUserType()) == 'coordinator') {
return true;
} else {
return false;
}
}
function IsUserUltimate()
{
if (strtolower(getUserType()) == 'ult') {
return true;
} else {
return false;
}
}
function IsUserOperator()
{
if (strtolower(getUserType()) == 'operator') {
return true;
} else {
return false;
}
}
function IsUserSuperOperator()
{
if (strtolower(getUserType()) == 'super operator') {
return true;
} else {
return false;
}
}
function IsUserUsher()
{
if (strtolower(getUserType()) == 'usher') {
return true;
} else {
return false;
}
}
function IsUserViewer()
{
if (strtolower(getUserType()) == 'viewer') {
return true;
} else {
return false;
}
}
function IsUserDisabler()
{
if (strtolower(getUserType()) == 'disabler') {
return true;
} else {
return false;
}
}
function IsNormalUser()
{
if (strtolower(getUserType()) == 'user') {
return true;
} else {
return false;
}
}
function CurrentUserUID()
{
$loginstatus = loginstatus()['userdata']['uid'] ?? false;
return $loginstatus;
}
function loginnow($username, $password, $keep_alive = false)
{
if (!$username or !$password) {
return false;
}
if (loginstatus()) {
return loginstatus();
}
$trylogin = trylogin($username, $password);
if (!$trylogin) {
return false;
}
$userid = $trylogin['uid'];
if ($keep_alive) {
$expiry = date("Y-m-d H:i:s", strtotime('+ 10 years'));
} else {
$expiry = date("Y-m-d H:i:s", strtotime('+ 2 hours'));
}
$newsessionkey = NewSession($userid, 1, $expiry);
$_SESSION['TA']['SESSIONKEY'] = $newsessionkey;
return $newsessionkey;
}
function logoutnow()
{
deleteAllUserSessions(CurrentUserUID());
unset($_SESSION['TA']);
}
function NewSession($userid, $active = 1, $expiry = false)
{
$user = checkifexists('users', ['uid' => $userid], ['hashkey', 'nickname', 'acct_type', 'mnumber', 'active', 'parentuid', 'targetuids', 'multiple_logins']);
if (!$user) {
return 'NO USER';
}
if (checkifexists('sessions', ['userid' => $userid, 'active' => 1], $fieldstoselectarray = '')) {
if (!$user['multiple_logins']) {
deleteAllUserSessions($userid);
}
}
if (!$expiry) {
$expiry = date("Y-m-d H:i:s", strtotime('+10 years'));
}
unset($user['password']);
unset($user['creation_date']);
unset($user['modified_date']);
$newhash = generatesessionhash();
$data['hashkey'] = $newhash;
$data['userid'] = $userid;
$data['active'] = $active;
$data['expiry'] = $expiry;
$data['userdata'] = json_encode($user);
$usertype = user_access($user['acct_type']);
$data['accesslist'] = json_encode($usertype);
$key = insertintodb(DB(), 'sessions', $data);
$sessiondata = getSessionData($newhash);
if (!$sessiondata) {
return false;
}
$sessionHistory = NewSession_History($userid, $sessiondata, $active, $expiry);
if (!$sessionHistory) {
deleteSession($newhash);
return false;
}
return $newhash;
}
function getSessionData($sessionhash)
{
$sessionhash = checkifexists('sessions', ['hashkey' => $sessionhash]);
if ($sessionhash and is_array($sessionhash) and !empty($sessionhash)) {
$sessiondata['userdata'] = json_decode($sessionhash['userdata'], 1);
$userhashkey = $sessiondata['userdata']['hashkey'];
$fieldstoselect = [
'uid',
'hashkey',
'nickname',
'acct_type',
'mnumber',
'active',
'parentuid',
'targetuids'
];
$sessionhash['userdata'] = GetUserDatabyUID($userhashkey, $fieldstoselect);
$usertype = $sessionhash['userdata']['acct_type'];
$sessionhash['accesslist'] = user_access($usertype);
return $sessionhash;
} else {
return false;
}
}
function getActiveSessionData($sessionhash)
{
$session = getSessionData($sessionhash);
if ($session and $session['active'] === 1) {
return $session;
} else {
return false;
}
}
function deleteSession($sessionhash)
{
deletefromdb('sessions', ['hashkey' => $sessionhash]);
if (checkifexists('sessions', ['hashkey' => $sessionhash], ['hashkey'])) {
return false;
}
return true;
}
function deleteAllUserSessions($userid)
{
deletefromdb('sessions', ['userid' => $userid]);
if (checkifexists('sessions', ['userid' => $userid], ['hashkey'])) {
return false;
}
return true;
}
function FindCurrentSessionForUser($userid)
{
$search = checkifexists('sessions', ['userid' => $userid, 'active' => 1], ['hashkey']);
if ($search and isset($search['hashkey'])) {
return $search['hashkey'];
}
return false;
}
function ModifySession($sessionhash, $newdata)
{
if (!$sessionhash) {
return false;
}
$check = checkifexists('sessions', ['hashkey' => $sessionhash]);
if (!$check or empty($check['hashkey'] or !$check['hashkey'])) {
return false;
}
$whereArray = ['hashkey' => $sessionhash];
return updatedbsimple(DB(), 'sessions', $newdata, $whereArray);
}
function ExtendSession($sessionhash, $newexpiry = false)
{
if (!$newexpiry) {
$newexpiry = date("Y-m-d H:i:s", strtotime('+3 days'));
}
return ModifySession($sessionhash, ['expiry' => $newexpiry]);
}
function ExtendSessionbyUID($UID, $newexpiry = false)
{
if (!$newexpiry) {
$newexpiry = date("Y-m-d H:i:s", strtotime('+3 days'));
}
$usersession = FindCurrentSessionForUser($UID);
return ModifySession($usersession, ['expiry' => $newexpiry]);
}
//SessionHistory
function NewSession_History($userid, $sessiondata, $active = 1, $expiry = false)
{
$newhash = generatesessionhash();
$data['hashkey'] = $newhash;
$data['userid'] = $sessiondata['userid'];
$data['old_hashkey'] = $sessiondata['hashkey'];
$data['active'] = $sessiondata['active'];
$data['expiry'] = $sessiondata['expiry'];
$data['accesslist'] = $sessiondata['accesslist'];
$data['userdata'] = $sessiondata['userdata'];
$data['login_time'] = serverdatetimesql();
$data['serverdata'] = json_encode($_SERVER);
$data['ip_address'] = $_SERVER['REMOTE_ADDR'];
$key = insertintodb(DB(), 'session_history', $data);
if (!$key) {
return false;
}
return $key;
}
function NewLog($log_type, $log_category, $description, $useruid = '')
{
if (!$useruid) {
$useruid = CurrentUserUID();
}
$data['log_time'] = serverdatetimesql();
$data['log_type'] = $log_type;
$data['log_category'] = $log_category;
$data['description'] = $description;
$data['server_data'] = json_encode($_SERVER);
$data['session_data'] = json_encode($_SESSION);
$data['useruid'] = $useruid;
$key = insertintodb(DB(), 'logs', $data);
if (!$key) {
return false;
}
return $key;
}
?>

View File

@@ -0,0 +1,945 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Helpers\Permissions;
use App\Enums\UserTypes;
use Hypervel\Http\Request;
use App\Models\User;
use Hypervel\Support\Facades\Auth;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\QueryHelper;
use App\Models\Market\Product;
use App\Models\Market\Store;
use Exception;
class LibLegacy
{
function endsWithSlash($path)
{
if (strlen($path) === 0) {
return false;
}
$lastChar = substr($path, -1);
return $lastChar === '/' || $lastChar === '\\';
}
function ArraytoHash($array)
{
if (!is_array($array) || empty($array)) {
return false;
}
$jsonString = json_encode($array);
$hash = hash('sha256', $jsonString);
return $hash;
}
function SetNoCache()
{
header('Cache-Control: no-cache');
}
function SetCacheTime($seconds)
{
if ($seconds) {
return false;
}
header('Cache-Control: max-age=' . $seconds . '');
}
function SetCacheTimeMinutes($minutes)
{
if ($minutes) {
return false;
}
$seconds = $minutes * 60;
header('Cache-Control: max-age=' . $seconds . '');
}
function SetCache1Year()
{
header('Cache-Control: max-age=31536000, public');
}
function json_array_echo($array)
{
jsonheader();
echo json_encode($array);
}
function tryjsondecode($string, $arrayoutput = true)
{
if (is_array($string)) {
return $string;
}
if (!$string) {
return $string;
}
$json = json_decode($string, $arrayoutput);
if ($json === null) {
return $string;
} else {
return $json;
}
}
function tryjsonencode($array)
{
if (!$array) {
return json_encode([]);
}
if (is_array($array)) {
$result = json_encode($array);
} else {
$result = json_encode([$array]);
}
return $result;
}
function filterArrayColumns($array, $columns)
{
return array_map(function ($item) use ($columns) {
return array_intersect_key($item, array_flip($columns));
}, $array);
}
function logmaker($filename, $nolog = false)
{
$main = new class ($filename, $nolog) {
public $filename;
public $loglistarray = [];
public function __construct($filename, $nolog)
{
$this->filename = $filename;
if ($nolog) {
return null;
}
}
public function add($str)
{
$this->loglistarray[] = $str;
}
public function done()
{
file_put_contents($this->filename, implode("\r\n\r\n", $this->loglistarray));
}
function __destruct()
{
file_put_contents($this->filename, implode("\r\n\r\n", $this->loglistarray));
}
};
return $main;
}
function createThumbnail($sourcePath, $destinationPath = null, $thumbWidth = 64, $thumbHeight = 64)
{
list($width, $height, $type) = getimagesize($sourcePath);
$srcImage = null;
switch ($type) {
case IMAGETYPE_JPEG:
$srcImage = imagecreatefromjpeg($sourcePath);
break;
case IMAGETYPE_PNG:
$srcImage = imagecreatefrompng($sourcePath);
break;
case IMAGETYPE_GIF:
$srcImage = imagecreatefromgif($sourcePath);
break;
default:
throw new Exception('Unsupported image type');
}
$thumbImage = imagecreatetruecolor($thumbWidth, $thumbHeight);
imagecopyresampled($thumbImage, $srcImage, 0, 0, 0, 0, $thumbWidth, $thumbHeight, $width, $height);
if (!$destinationPath) {
$basepath = dirname($sourcePath);
$basenameWithoutExtension = pathinfo($sourcePath, PATHINFO_FILENAME);
$extension = pathinfo($sourcePath, PATHINFO_EXTENSION) ?? '';
if ($extension) {
$extension = '.' + $extension;
}
$newfilename = $basenameWithoutExtension + '_thumbnail' + $extension;
$destinationPath = $basepath + '/' + $newfilename;
}
switch ($type) {
case IMAGETYPE_JPEG:
imagejpeg($thumbImage, $destinationPath);
break;
case IMAGETYPE_PNG:
imagepng($thumbImage, $destinationPath);
break;
case IMAGETYPE_GIF:
imagegif($thumbImage, $destinationPath);
break;
}
imagedestroy($srcImage);
imagedestroy($thumbImage);
}
function trim_ending_hyphen($string)
{
if (substr($string, -1) === "-") {
return substr($string, 0, -1); // Remove the last character (hyphen)
} else {
return $string; // Return the original string if it doesn't end with a hyphen
}
}
function convertStringToSQLDateTime($stringDate)
{
$timestamp = strtotime($stringDate);
$sqlDatetime = date("Y-m-d H:i:s", $timestamp);
return $sqlDatetime;
}
function comparestringnumberstoarray($string, $comparearray, $inorder = false)
{
$mainarray = explode('-', $string);
$mainarray = array_values($mainarray);
$lastvaluemainarray = $mainarray[count($mainarray) - 1];
if ($lastvaluemainarray === '') {
array_pop($mainarray);
}
if (in_array('', $mainarray)) {
return null;
}
if (!$inorder) {
sort($mainarray);
sort($comparearray);
}
return $mainarray == $comparearray;
}
function refreshpage()
{
return '<script>location.reload()</script>';
}
function refreshpagenopost()
{
return '<script>window.location = window.location.href</script>';
}
function refresh()
{
header('Location: .');
}
function jschangetopurl($url)
{
return '<script>top.location = "' . $url . '";</script>';
}
function jschangetopurlnoscripttag($url)
{
return "top.location = '" . $url . "';";
}
function jsopenmodal($modalname)
{
return '<script>
$(document).ready(function(){
$("#' . $modalname . '").modal();
});
</script>';
}
function jsonheader()
{
header('Content-Type: application/json; charset=utf-8');
}
function simpleredirect($requesturl, $functiontoexecute, $reqtype = '', $fallbackfunction = NULL, $conditiontrue = NULL, $caching = false)
{
// sample url = http://website.com/?u=name/{john}
// sample function syntax simpleredirect('name/{}')
if (file_exists('sim.txt')) {
unlink('sim.txt');
}
$addlog = function ($str) {
@file_put_contents('sim.txt', $str . "\r\n", FILE_APPEND);
};
$urlcheck = geturlparameters($requesturl);
$addlog('chekingurl=' . getappendedurl() . ' requrl=' . $requesturl);
if ($urlcheck === FALSE) {
$addlog('urlcheck=false');
return false;
}
$addlog('chekingurl success');
if ($reqtype == '') {
$reqtype = 'get';
$addlog('reqtype unknown changing to get');
} else {
$reqtype = strtolower($reqtype);
$addlog('reqtype = ' . $reqtype);
}
if ($reqtype !== 'post' and $reqtype !== 'get') {
$reqtype = 'get';
$addlog('reqtype not post or get changing to get');
}
if (strtolower($reqtype) !== strtolower($_SERVER['REQUEST_METHOD'])) {
$addlog('reqtype not the same reqtype = ' . $reqtype . ' requestmethod = ' . $_SERVER['REQUEST_METHOD']);
return FALSE;
}
$GLOBALS['urlparameters'] = $urlcheck;
if ($conditiontrue !== NULL and !$conditiontrue) {
$addlog('condition not true exiting');
if ($fallbackfunction) {
$fallbackfunction();
}
return FALSE;
}
if (!$caching) {
$addlog('removing cache');
removecaching();
} elseif ($caching) {
$addlog('adding cache');
if ($caching === true) {
SetCache1Year();
} elseif (is_numeric($caching)) {
SetCacheTimeMinutes($caching);
} else {
SetCache1Year();
}
}
$addlog('executing function');
$functiontoexecute($urlcheck);
}
function simpleredirectfile($requesturl, $file, $reqtype = '', $fallbackfunction = NULL, $conditiontrue = NULL, $caching = FALSE)
{
$filefunct = function () use ($file) {
readfile($file);
};
simpleredirect($requesturl, $filefunct, $reqtype, $fallbackfunction, $conditiontrue, $caching);
}
function checkifstringisenclosedinbrackets($string)
{
// checks if string is enclosed in brackers ex {name}
//returns string inside and returns false if there is no bracket
if (substr($string, 0, 1) === "{" && substr($string, -1) === "}") {
$string = substr($string, 1, -1);
} else {
$string = false;
}
return $string;
}
function geturlparameters($urlparam)
{
//urlparam = 'users/'
if ($urlparam === '') {
$urlparam = [];
} else {
$urlparam = explode('/', $urlparam);
}
$url = geturlarray();
if (count($url) !== count($urlparam)) {
return false;
}
$params = [];
foreach ($urlparam as $key => $value) {
if ($urlparam[$key] !== $url[$key]) {
$enclosed = checkifstringisenclosedinbrackets($value);
if ($enclosed) {
$params[$enclosed] = $url[$key];
} else {
return false;
}
}
}
return $params;
}
function getLastDigitSegments($string, $n)
{
$parts = explode('-', $string);
$lastSegments = array_slice($parts, -$n);
return implode('-', $lastSegments);
}
function geturlarray()
{
$url = getappendedurl();
if (substr($url, 0, 1) === "/") {
$uri = substr($url, 1);
} else {
$uri = $url;
}
$uri = array_values(array_filter(explode('/', $uri)));
unset($uri[0]);
$uri = array_values($uri);
if (empty($uri)) {
return [];
}
if (substr($uri[0], 0, 1) === "?") {
$uri[0] = substr($uri[0], 1);
}
return $uri;
}
function getappendedurl()
{
//get appended url sample url https:/google.com/aslkdfj/asdf/asdf
//result aslkdfj/asdf/asdf
return $_SERVER['REQUEST_URI'];
}
function removecaching()
{
if (headers_sent()) {
return false;
}
header_remove('ETag');
header_remove('Pragma');
header_remove('Cache-Control');
header_remove('Last-Modified');
header_remove('Expires');
// set header
header('Expires: Thu, 1 Jan 1970 00:00:00 GMT');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
}
function is_valid_var($var)
{
if (!isset($var)) {
return null;
}
if (empty($var) || $var === false || is_null($var)) {
return null;
}
return $var;
}
function ConvertDatetoMDYYYY($datestring)
{
//from YYYY-MM-DD to M/D/YYYY
if (!$datestring) {
return false;
}
$timestamp = strtotime($datestring);
$formattedDate = date('n/j/Y', $timestamp);
return $formattedDate;
}
function PNGtoJPGNN($filelocation, $newfilelocation)
{
$new_pic = imagecreatefrompng($filelocation);
$w = imagesx($new_pic);
$h = imagesy($new_pic);
$white = imagecreatetruecolor($w, $h);
$bg = imagecolorallocate($white, 255, 255, 255);
imagefill($white, 0, 0, $bg);
imagecopy($white, $new_pic, 0, 0, 0, 0, $w, $h);
$success = imagejpeg($white, $newfilelocation);
imagedestroy($new_pic);
imagedestroy($white);
return $success;
}
function PNGtoJPG($pngFilePath, $jpgFilePath, $maxWidth = false, $maxHeight = false, $quality = 100)
{
$pngImage = imagecreatefrompng($pngFilePath);
if (!$pngImage) {
return false;
}
$originalWidth = imagesx($pngImage);
$originalHeight = imagesy($pngImage);
if (!$maxWidth && !$maxHeight) {
$maxWidth = $originalWidth;
$maxHeight = $originalHeight;
}
$aspectRatio = $originalWidth / $originalHeight;
if ($maxWidth / $maxHeight > $aspectRatio) {
$newWidth = $maxHeight * $aspectRatio;
$newHeight = $maxHeight;
} else {
$newWidth = $maxWidth;
$newHeight = $maxWidth / $aspectRatio;
}
$newWidth = ceil($newWidth);
$newHeight = ceil(num: $newHeight);
$jpgImage = imagecreatetruecolor($newWidth, $newHeight);
imagecopyresampled($jpgImage, $pngImage, 0, 0, 0, 0, $newWidth, $newHeight, $originalWidth, $originalHeight);
$success = imagejpeg($jpgImage, $jpgFilePath, $quality);
imagedestroy($pngImage);
imagedestroy($jpgImage);
return $success;
}
function PNGtoWebP($pngFilePath, $webpFilePath, $maxWidth = false, $maxHeight = false, $quality = 100)
{
$pngImage = imagecreatefrompng($pngFilePath);
if (!$pngImage) {
return false;
}
$originalWidth = imagesx($pngImage);
$originalHeight = imagesy($pngImage);
if (!$maxWidth && !$maxHeight) {
$maxWidth = $originalWidth;
$maxHeight = $originalHeight;
}
$aspectRatio = $originalWidth / $originalHeight;
if ($maxWidth / $maxHeight > $aspectRatio) {
$newWidth = $maxHeight * $aspectRatio;
$newHeight = $maxHeight;
} else {
$newWidth = $maxWidth;
$newHeight = $maxWidth / $aspectRatio;
}
$newWidth = ceil($newWidth);
$newHeight = ceil(num: $newHeight);
$webpImage = imagecreatetruecolor($newWidth, $newHeight);
imagealphablending($webpImage, false);
imagesavealpha($webpImage, true);
$transparent = imagecolorallocatealpha($webpImage, 255, 255, 255, 127);
imagefilledrectangle($webpImage, 0, 0, $newWidth, $newHeight, $transparent);
imagecopyresampled($webpImage, $pngImage, 0, 0, 0, 0, $newWidth, $newHeight, $originalWidth, $originalHeight);
$success = imagewebp($webpImage, $webpFilePath, $quality);
imagedestroy($pngImage);
imagedestroy($webpImage);
return $success;
}
function BatchConvertPNG($oldfolder, $newfolder, $newtype = 'webp', $maxWidth = false, $maxHeight = false, $quality = 100)
{
//filename from old folder to new folder is copied exactly even the extensions are the same.
if (!$newtype) {
$newtype = 'webp';
}
$oldfolder = AddSlashtoStrifNolastSlash($oldfolder);
$newfolder = AddSlashtoStrifNolastSlash($newfolder);
$processIMG = function ($oldfilename) use ($newfolder, $newtype, $maxWidth, $maxHeight, $quality) {
if (!file_exists($oldfilename)) {
echo $oldfilename . '<br>';
return false;
}
$mimetype = mime_content_type($oldfilename);
if (!str_contains($mimetype, 'png')) {
return false;
}
$basefilename = basename($oldfilename);
$newjpglocation = $newfolder . $basefilename;
if (strtolower($newtype) === 'webp') {
return PNGtoWebP($oldfilename, $newjpglocation, $maxWidth, $maxHeight, $quality);
} elseif (strtolower($newtype) === 'jpg' || strtolower($newtype) === 'jpeg') {
return PNGtoJPG($oldfilename, $newjpglocation, $maxWidth, $maxHeight, $quality);
} else {
return false;
}
};
$dir = scandir($oldfolder);
foreach ($dir as $keyy => $file) {
if ($file === '..') {
unset($dir[$keyy]);
continue;
}
if ($file === '.') {
unset($dir[$keyy]);
continue;
}
}
foreach ($dir as $file) {
$oldfilename = $oldfolder . '/' . $file;
$processIMG($oldfilename);
}
}
function AddSlashtoStrifNolastSlash($str)
{
if (!$str) {
return false;
}
$lastchar = substr($str, -1);
if ($lastchar !== '/' && $lastchar !== '\\') {
$str .= '/';
}
return $str;
}
function formatDateTimetoReadable($dateTimeString)
{
if (empty($dateTimeString)) {
return '';
}
$date = new DateTime($dateTimeString);
if (!$date) {
return '';
}
$formattedDate = $date->format('F j, Y');
$hours = (int) $date->format('g');
$minutes = (int) $date->format('i');
$ampm = $date->format('A');
$formattedMinutes = str_pad($minutes, 2, '0', STR_PAD_LEFT);
return strpos($dateTimeString, ' ') !== false
? "{$formattedDate} {$hours}:{$formattedMinutes}{$ampm}"
: "{$formattedDate}";
}
function generateDatesOLD($daysArray, $months = 1, $sort = true)
{
if (!$daysArray) {
return false;
}
if (!is_array($daysArray)) {
$daysArray = tryjsondecode($daysArray);
}
if (!is_array($daysArray)) {
return false;
}
$dates = [];
// Iterate through each day in the array
foreach ($daysArray as $day) {
list($weekday, $time) = $day;
$currentDate = new DateTime();
$currentDate->modify("next $weekday");
for ($i = 0; $i < $months; $i++) {
// Add the time to the current date
$dateWithTime = $currentDate->format('Y-m-d') . ' ' . $time;
// Add the date to the array
$dates[] = $dateWithTime;
// Move to the next week (incrementing only the week part)
$currentDate->modify('+1 week');
}
}
// Sort the dates if specified
if ($sort) {
sort($dates);
}
return $dates;
}
function generateDatesNEW($daysArray, $months = 2, $sort = true)
{
if (!$daysArray) {
return false;
}
if (!is_array($daysArray)) {
$daysArray = tryjsondecode($daysArray);
}
if (!is_array($daysArray)) {
return false;
}
$dates = [];
// Iterate through each day in the array
foreach ($daysArray as $day) {
list($weekday, $time) = $day;
$currentDate = new DateTime();
// If today is the specified weekday, include it
if (strtolower($currentDate->format('D')) === strtolower($weekday)) {
$dateWithTime = $currentDate->format('Y-m-d') . ' ' . $time;
$dates[] = $dateWithTime;
}
// Move to the next week (incrementing only the week part)
$currentDate->modify("next $weekday");
for ($i = 1; $i < $months; $i++) {
// Add the time to the current date
$dateWithTime = $currentDate->format('Y-m-d') . ' ' . $time;
// Add the date to the array
$dates[] = $dateWithTime;
// Move to the next week (incrementing only the week part)
$currentDate->modify('+1 week');
}
}
// Sort the dates if specified
if ($sort) {
sort($dates);
}
return $dates;
}
function generateDates($daysArray, $days = 8, $sort = true)
{
$dates = generateDatesNEW($daysArray, $months = 4, $sort);
unset($dates[0]);
return array_splice($dates, 0, $days, []);
}
function GetTotalAmountfromArray($array)
{
if (!is_array($array)) {
return false;
}
$total = 0;
foreach ($array as $amount) {
if (is_numeric($amount) && $amount !== '') {
$total += $amount;
}
}
return $total;
}
function removecolumnsfrom2Darray($array, $arraycolumnstoremove)
{
if (!is_array($array) or !is_array($arraycolumnstoremove)) {
return false;
}
foreach ($array as $key => $value) {
foreach ($value as $skey => $sval) {
if (in_array($skey, $arraycolumnstoremove)) {
unset($array[$key][$skey]);
}
}
}
}
function sqlarray_2dfilter($array, $columnname, string $stringtosearch, $exact = FALSE, $caseinsensitive = false, $simplesearchwith_strpos = false)
{
if ($columnname === '' or $columnname === null or $columnname === false) {
return FALSE;
}
if (!$array) {
return false;
}
$datatosearch = array_column($array, $columnname);
if ($caseinsensitive) {
$stringtosearch = strtolower($stringtosearch);
foreach ($datatosearch as $key => $value) {
$datatosearch[$key] = strtolower($value);
}
}
if ($exact) {
$keylist = array_keys($datatosearch, $stringtosearch);
} else {
if ($simplesearchwith_strpos) {
$keylist = [];
foreach ($datatosearch as $sskey => $ssvalue) {
if ($caseinsensitive) {
if (stripos($ssvalue, $stringtosearch)) {
$keylist[] = $sskey;
}
} elseif (!$caseinsensitive) {
if (strpos($ssvalue, $stringtosearch)) {
$keylist[] = $sskey;
}
}
}
} else {
$stringtosearch = preg_quote($stringtosearch, '/'); // remove if an error is encountered
$keylist = array_keys(preg_grep('/(?i)' . $stringtosearch . '/', $datatosearch));
}
}
$res = [];
foreach ($keylist as $value) {
$res[] = $array[$value];
}
return $res;
}
function array2d_removeduplicate($array, $columnname, $reorder = FALSE)
{
$keylist = array_keys(array_unique(array_column($array, $columnname)));
$newarray = [];
foreach ($keylist as $value) {
$newarray[$value] = $array[$value];
}
if ($reorder) {
$newarray = array_values($newarray);
}
return $newarray;
}
/**
* Searches a 2d array using a multiple words that match by separating by space.
*
* This function takes a 2D array, a column name, and a search string as input.
* It separates the search string by space and calls the sqlarray_2dfilter function
* multiple times until all words are matched.
*
* @param array $array The 2D array to search in.
* @param string $columnname The name of the column to search in.
* @param string $stringtosearch The search string to search for.
* @param bool $exact Whether to search for exact matches or not. Defaults to FALSE.
* @param bool $caseinsensitive Whether to perform a case-insensitive search or not. Defaults to FALSE.
* @param bool $simplesearchwith_strpos Whether to use the strpos function for searching or not. Defaults to FALSE.
*
* @return array The filtered data array where all words in the search string are present.
*/
function sqlarray_2dfilter_multiple($array, $columnname, string $stringtosearch, $exact = FALSE, $caseinsensitive = false, $simplesearchwith_strpos = false)
{
if ($columnname === '' or $columnname === null or $columnname === false) {
return FALSE;
}
if (!$array) {
return false;
}
$words = explode(' ', $stringtosearch);
$result = $array;
foreach ($words as $word) {
$result = sqlarray_2dfilter($result, $columnname, $word, $exact, $caseinsensitive, $simplesearchwith_strpos);
if (empty($result)) {
return [];
}
}
return $result;
}
function sqlarray_2dfilterContinuos($array, $columnname, $stringarraytosearch, $exact = FALSE, $caseinsensitive = false)
{
$res = [];
foreach ($stringarraytosearch as $key => $value) {
$searchresult = sqlarray_2dfilter($array, $columnname, $value, $exact, $caseinsensitive);
if (!$searchresult) {
continue;
}
$res = [...$res, ...$searchresult];
}
return $res;
}
function encryptString($simple_string, $encryption_key)
{
$ciphering = "AES-128-CTR";
$iv_length = openssl_cipher_iv_length($ciphering);
$options = 0;
$encryption_iv = '1218277893585121';
$encryption = openssl_encrypt(
$simple_string,
$ciphering,
$encryption_key,
$options,
$encryption_iv
);
return $encryption;
}
function decryptString($string, $decryption_key)
{
$ciphering = "AES-128-CTR";
$decryption_iv = '1218277893585121';
$options = 0;
$decryption = openssl_decrypt(
$string,
$ciphering,
$decryption_key,
$options,
$decryption_iv
);
return $decryption;
}
function objectToUrlSafeBase64($obj)
{
$jsonString = json_encode($obj);
$base64String = base64_encode($jsonString);
$urlSafeBase64 = str_replace(['+', '/', '='], ['-', '_', ''], $base64String);
return $urlSafeBase64;
}
function urlSafeBase64ToObject($urlSafeBase64)
{
$base64String = str_replace(['-', '_'], ['+', '/'], $urlSafeBase64);
$padding = strlen($base64String) % 4;
if ($padding) {
$base64String .= str_repeat('=', 4 - $padding);
}
$jsonString = base64_decode($base64String);
return json_decode($jsonString);
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Helpers;
use App\Models\FileList;
use Hypervel\Http\Request;
use App\Http\Controllers\FilesMainController;
class ObjectToBase64Helper
{
public static function urlSafeBase64ToObject(string $urlSafeBase64, bool $compressed = true)
{
if (!is_string($urlSafeBase64)) {
return false;
}
$base64 = str_replace(['-', '_'], ['+', '/'], $urlSafeBase64);
$padding = strlen($base64) % 4;
if ($padding !== 0) {
$base64 .= str_repeat('=', 4 - $padding);
}
// Base64 decode
$binary = base64_decode($base64, true);
if ($binary === false) {
return null;
}
try {
if ($compressed) {
// Equivalent of pako.inflate()
$json = gzuncompress($binary);
if ($json === false) {
return null;
}
return json_decode($json, true);
}
return json_decode($binary, true);
} catch (\Throwable $e) {
return null;
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Helpers;
/**
* Mock Payment Processor Helper for Co-op Credit Top-ups
*/
class PaymentProcessor
{
/**
* Simulate a top-up request to an external processor
*/
public static function initiatePayment(float $amount, string $method, string $userHash)
{
// In a real scenario, this would call GCash/PayMaya API
// For now, we return a mock response
return [
'success' => true,
'transaction_id' => 'TXN-' . strtoupper(bin2hex(random_bytes(6))),
'redirect_url' => null, // Would be used for hosted payment pages
'status' => 'PENDING',
];
}
/**
* Complete a payment after external confirmation
*/
public static function completePayment(string $transactionId)
{
return [
'success' => true,
'status' => 'COMPLETED',
];
}
}

View File

@@ -0,0 +1,191 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Helpers\Permissions;
use App\Enums\UserTypes;
use Hypervel\Http\Request;
use App\Models\User;
use Hypervel\Support\Facades\Auth;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\QueryHelper;
use App\Models\Market\Product;
use App\Models\Market\Store;
use Exception;
class ProductPermissions
{
public static function isModificationAllowed()
{
}
public static function isCreationAllowed()
{
}
public static function isActionAllowed(
UserActions $userAction,
Product|string|int|null $productHashorID = null,
Store|string|int|null $storeHashorID = null
): bool {
try {
$acct_type = Auth::user()->acct_type;
} catch (Exception $e) {
return false;
}
$defaultRoles = ProductPermissionsDefinition::getAllowedUserTypesAction($acct_type);
$additionalRoles = UserPermissions::isUserAllowedbyAdditionalRoles($userAction);
$deniedRoles = UserPermissions::isUserDeniedRoles($userAction);
if ($deniedRoles) {
return false;
}
if (!in_array($userAction, $defaultRoles, true) && !$additionalRoles) {
return false;
}
if (!ProductPermissionsDefinition::doesActionRequireDirectChildren($userAction)) {
return true;
}
if (!$storeHashorID && !$productHashorID) {
return false;
}
$store = null;
$product = null;
if ($storeHashorID) {
$store = QueryHelper::findOrNullByHashOrId($storeHashorID, Store::class);
}
if ($productHashorID) {
$product = QueryHelper::findOrNullByHashOrId($productHashorID, Product::class);
}
if (!$store && !$product) {
return false;
}
// Determine store from product if needed
if (!$store && $product) {
$store = $product->store ?? null;
}
if (!$store) {
return false;
}
$storeOwner = $store->owner;
if ($storeOwner && UserPermissions::isDescendantOfCurrentUser($storeOwner)) {
return true;
}
// Check all managers in the new store_managers table
$managerIds = $store->managerUsers()->pluck('users.id')->toArray();
foreach ($managerIds as $managerId) {
if (UserPermissions::isDescendantOfCurrentUser($managerId)) {
return true;
}
}
// Legacy manager check
if ($store->manager_id && UserPermissions::isDescendantOfCurrentUser($store->manager_id)) {
return true;
}
return false;
}
}
class ProductPermissionsDefinition
{
public static function getAllowedUserTypesAction(UserTypes $currentUserType)
{
return match ($currentUserType) {
UserTypes::ULTIMATE => UserActions::cases(),
UserTypes::SUPER_OPERATOR => [
UserActions::CreateStoreforSelf,
UserActions::CreateStoreGlobal,
UserActions::ModifyAllStores,
UserActions::ModifyOwnStore,
UserActions::CreateProductGlobal,
UserActions::CreateProductForOwnStore,
UserActions::CreateProductforSelf,
UserActions::ModifyAllProducts,
UserActions::ModifyOwnProduct,
UserActions::AddProducttoOwnStore,
UserActions::AddProducttoAnyStore,
UserActions::RemoveProductfromAnyStore,
],
UserTypes::OPERATOR => [
UserActions::CreateStoreforSelf,
UserActions::CreateStoreGlobal,
UserActions::ModifyAllStores,
UserActions::ModifyOwnStore,
UserActions::CreateProductGlobal,
UserActions::CreateProductForOwnStore,
UserActions::CreateProductforSelf,
UserActions::ModifyAllProducts,
UserActions::ModifyOwnProduct,
UserActions::AddProducttoOwnStore,
UserActions::AddProducttoAnyStore,
UserActions::RemoveProductfromAnyStore,
],
UserTypes::STORE_OWNER => [
UserActions::ModifyOwnStore,
UserActions::ModifyOwnProduct,
UserActions::AddProducttoOwnStore,
UserActions::CreateProductForOwnStore,
],
UserTypes::STORE_MANAGER => [
UserActions::ModifyOwnProduct,
UserActions::AddProducttoOwnStore,
UserActions::CreateProductForOwnStore,
],
default => [],
};
}
public static function doesActionRequireDirectChildren(UserActions $userAction)
{
return match ($userAction) {
UserActions::CreateStoreforSelf => true,
UserActions::CreateStoreGlobal => false,
UserActions::ModifyAllStores => false,
UserActions::ModifyOwnStore => true,
UserActions::CreateProductGlobal => false,
UserActions::CreateProductforSelf => true,
UserActions::ModifyAllProducts => false,
UserActions::ModifyOwnProduct => true,
UserActions::AddProducttoOwnStore=>true,
UserActions::AddProducttoAnyStore=>false,
UserActions::RemoveProductfromAnyStore=>false,
default => false,
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Helpers;
use App\Models\FileList;
use Hypervel\Http\Request;
use App\Http\Controllers\FilesMainController;
class PhotoURL
{
public static function getDropzoneDetailsbyHashkey(string $filelist_hashkey): array|false
{
$fileList = FileList::where('hashkey', $filelist_hashkey)->first();
if (!$fileList) {
return false;
}
$fileContent = $fileList->fileContent()->first();
$file_url = $fileList->resolvedUrl();
$filename = $fileList->filename ?? $fileContent?->titlename ?? 'unknown_file';
$thumbnail = null;
return [
'url' => $file_url,
'name' => $filename,
'size' => $fileContent->size_in_bytes ?? 0,
'hashkey' => $fileList->hashkey,
'thumbnail' => $thumbnail ?? $file_url,
];
}
public static function photoURLArraytoDropzoneData(array|false|null $PhotoURLArray)
{
if (empty($PhotoURLArray)) {
return false;
}
$dropzoneArray = [];
foreach ($PhotoURLArray as $hashkey) {
$dropzoneArray[] = self::getDropzoneDetailsbyHashkey($hashkey) ?? [];
}
return $dropzoneArray;
}
function generateThumbnailGD(string $sourcePath, string $destinationPath, int $width = 200, int $height = 200): bool
{
if (!file_exists($sourcePath)) {
return false;
}
$imageInfo = getimagesize($sourcePath);
if (!$imageInfo) return false;
[$srcWidth, $srcHeight] = $imageInfo;
$mime = $imageInfo['mime'];
// Create image resource from source
switch ($mime) {
case 'image/jpeg':
$srcImage = imagecreatefromjpeg($sourcePath);
break;
case 'image/png':
$srcImage = imagecreatefrompng($sourcePath);
break;
case 'image/gif':
$srcImage = imagecreatefromgif($sourcePath);
break;
case 'image/webp':
$srcImage = imagecreatefromwebp($sourcePath);
break;
default:
return false; // unsupported
}
// Calculate new dimensions preserving aspect ratio
$aspectRatio = $srcWidth / $srcHeight;
if ($width / $height > $aspectRatio) {
$width = (int)($height * $aspectRatio);
} else {
$height = (int)($width / $aspectRatio);
}
// Create the new thumbnail image
$thumbImage = imagecreatetruecolor($width, $height);
// Preserve transparency for PNG/GIF
if (in_array($mime, ['image/png', 'image/gif'])) {
imagecolortransparent($thumbImage, imagecolorallocatealpha($thumbImage, 0, 0, 0, 127));
imagealphablending($thumbImage, false);
imagesavealpha($thumbImage, true);
}
// Resize
imagecopyresampled($thumbImage, $srcImage, 0, 0, 0, 0, $width, $height, $srcWidth, $srcHeight);
// Save to file
switch ($mime) {
case 'image/jpeg':
imagejpeg($thumbImage, $destinationPath, 85);
break;
case 'image/png':
imagepng($thumbImage, $destinationPath);
break;
case 'image/gif':
imagegif($thumbImage, $destinationPath);
break;
case 'image/webp':
imagewebp($thumbImage, $destinationPath, 85);
break;
}
// Cleanup
imagedestroy($srcImage);
imagedestroy($thumbImage);
return true;
}
}

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Helpers;
/**
* Decoder for QRPH / EMV QRCPS strings.
*
* QRPH follows the EMVCo Merchant-Presented QR Code Specification.
* Each field is encoded as: 2-char tag + 2-char length (decimal) + value.
* Nested (sub-field) structures use the same encoding inside a parent value.
*/
class QrphDecoder
{
/**
* Decode a QRPH string and return human-readable fields.
*/
public static function decode(string $qrString): array
{
$qrString = trim($qrString);
$fields = self::parseTlv($qrString);
return [
'raw' => $qrString,
'valid' => self::validateCrc($qrString, $fields),
'initiation_method'=> self::initiationLabel($fields['01'] ?? null),
'merchant_name' => $fields['59'] ?? null,
'merchant_city' => $fields['60'] ?? null,
'country_code' => $fields['58'] ?? null,
'currency' => $fields['53'] ?? null, // 608 = PHP
'amount' => isset($fields['54']) ? (float) $fields['54'] : null,
'merchant_account' => self::extractMerchantAccount($fields),
];
}
/**
* Parse a flat TLV string into [ tag => value ] pairs.
*/
public static function parseTlv(string $data): array
{
$result = [];
$pos = 0;
$len = strlen($data);
while ($pos + 4 <= $len) {
$tag = substr($data, $pos, 2);
$length = (int) substr($data, $pos + 2, 2);
$value = substr($data, $pos + 4, $length);
$result[$tag] = $value;
$pos += 4 + $length;
if ($tag === '63') break; // CRC is always last
}
return $result;
}
/**
* Extract primary account info from merchant account fields (tags 2651).
* Returns the first one found with a recognisable GUID.
*/
private static function extractMerchantAccount(array $fields): ?array
{
for ($i = 26; $i <= 51; $i++) {
$tag = str_pad((string) $i, 2, '0', STR_PAD_LEFT);
if (!isset($fields[$tag])) continue;
$sub = self::parseTlv($fields[$tag]);
$guid = $sub['00'] ?? '';
return [
'guid' => $guid,
'network' => self::networkFromGuid($guid),
'account' => $sub['01'] ?? null,
'name' => $sub['02'] ?? null,
];
}
return null;
}
private static function networkFromGuid(string $guid): string
{
$map = [
// Standard QRPH / InstaPay (BSP / PPMI)
'com.p2pqrph' => 'InstaPay (QRPH)',
'ph.ppmi' => 'InstaPay',
'ph.bsp.qrph' => 'InstaPay (BSP)',
// GoTyme Bank
'com.gotyme' => 'GoTyme',
'ph.gotyme' => 'GoTyme',
'gotyme' => 'GoTyme',
// GCash
'com.gcash' => 'GCash',
// Maya / PayMaya
'com.paymaya' => 'Maya',
'com.maya' => 'Maya',
// Major PH banks
'ph.bpi' => 'BPI',
'ph.bdo' => 'BDO',
'com.landbank' => 'Landbank',
'ph.landbank' => 'Landbank',
'ph.ubp' => 'UnionBank',
'ph.metrobank' => 'Metrobank',
'ph.rcbc' => 'RCBC',
'ph.pnb' => 'PNB',
'ph.securitybank' => 'Security Bank',
'ph.chinabank' => 'China Bank',
'ph.eastwest' => 'EastWest Bank',
'ph.psbank' => 'PSBank',
'ph.boc' => 'Bank of Commerce',
'com.seabank' => 'SeaBank',
'com.tonik' => 'Tonik',
'com.uno' => 'UNO Digital Bank',
'com.komo' => 'CIMB / Komo',
'com.ownbank' => 'OwnBank',
];
$lower = strtolower($guid);
foreach ($map as $key => $label) {
if (str_contains($lower, $key)) return $label;
}
// Fallback: return the raw GUID so it's still visible
return $guid ?: 'Unknown';
}
private static function initiationLabel(?string $code): string
{
return match ($code) {
'11' => 'Static',
'12' => 'Dynamic',
default => $code ?? 'Unknown',
};
}
/**
* Validate the CRC-16/CCITT-FALSE checksum embedded in the QR string.
*/
public static function validateCrc(string $qrString, array $parsed = []): bool
{
if (empty($parsed['63'])) return false;
// CRC covers everything up to and including the "6304" tag+length prefix.
$crcPos = strrpos($qrString, '6304');
if ($crcPos === false) return false;
$payload = substr($qrString, 0, $crcPos + 4);
$expected = strtoupper($parsed['63']);
$computed = self::crc16($payload);
return $computed === $expected;
}
/**
* CRC-16/CCITT-FALSE (polynomial 0x1021, seed 0xFFFF).
*/
public static function crc16(string $data): string
{
$crc = 0xFFFF;
for ($i = 0, $l = strlen($data); $i < $l; $i++) {
$crc ^= (ord($data[$i]) << 8);
for ($j = 0; $j < 8; $j++) {
$crc = ($crc & 0x8000)
? (($crc << 1) ^ 0x1021) & 0xFFFF
: ($crc << 1) & 0xFFFF;
}
}
return sprintf('%04X', $crc);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Helpers;
use App\Models\FileList;
use Hypervel\Http\Request;
use App\Http\Controllers\FilesMainController;
use Hypervel\Database\Eloquent\Model;
class QueryHelper
{
public static function findOrNullByHashOrId(
int|string|Model|false $hashOrId,
string $modelClass
): false|Model {
if (!$hashOrId) {
return false;
}
if ($hashOrId instanceof Model) {
return $hashOrId;
}
if (!is_subclass_of($modelClass, Model::class)) {
throw new \InvalidArgumentException('Invalid model class');
}
if (is_int($hashOrId)) {
return $modelClass::find($hashOrId);
}
return $modelClass::where('hashkey', $hashOrId)->first();
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Helpers;
use Hypervel\Http\Request;
class ResponseHelper
{
public static function returnSuccessResponse($data, $hashkey,$message='')
{
return response()->json([
'success' => true,
'data' => $data,
'hashkey' => $hashkey,
'message'=>$message,
]);
}
public static function returnFalseOrNull($hashfromRequest)
{
if ($hashfromRequest) {
return response()->json(['success' => false]);
} else {
return null;
}
}
public static function returnUnauthorized()
{
return response()->json(['success' => false, 'message' => 'Unauthorized '], 403);
}
public static function returnIncorrectDetails()
{
return self::returnError('Incomplete/Incorrect Details!');
}
public static function returnError($errorText, $status = 500, $data = [])
{
return response()->json(['success' => false, 'message' => $errorText, 'data' => $data], $status);
}
public static function getTargetHash()
{
$target = request()->input('target');
if (!$target) {
return null;
}
if (is_object($target) || is_array($target)) {
try {
$target = $target['target'];
return $target;
} catch (\Throwable $th) {
return null;
}
}
return $target;
}
public static function getTargetData()
{
$target = request()->input('data');
return $target;
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Helpers;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
class UserController
{
public static function findUserIdByHash(?string $hash): ?int
{
if (!$hash || !is_string($hash)) {
return null;
}
$user = User::where('hashkey', $hash)->first();
return $user?->id;
}
//TODO Test this function
public static function getCurrentUserDescendants()
{
try {
return Auth::user()->getAllDescendants();
} catch (\Throwable $th) {
return false;
}
}
public static function getUserDescendantsbyHash(string $hashkey)
{
try {
$user = User::where('hashkey', $hashkey)->firstOrFail();
return $user->getAllDescendants();
} catch (\Throwable $th) {
return false;
}
}
public static function checkifUserHashisADescendantofCurrentUser(string $hashkey)
{
try {
$currentUser = Auth::user();
if (!$currentUser)
return false;
return $currentUser
->getAllDescendants()
->pluck('hashkey')
->contains($hashkey);
} catch (\Throwable $th) {
return false;
}
}
public static function checkifUserHashisAChildofCurrentUser(string $hashkey)
{
try {
$currentUser = Auth::user();
if (!$currentUser) {
return false;
}
return User::where('parentuid', $currentUser->id)
->where('hashkey', $hashkey)
->exists();
} catch (\Throwable $th) {
return false;
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Hypervel\Http\Request;
class IndexController extends AbstractController
{
public function index(Request $request): array
{
$user = $request->input('user', 'Hypervel');
$method = $request->getMethod();
return [
'method' => $method,
'message' => "Hello {$user}.",
];
}
}

View File

@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Cache;
use Hypervel\Support\Facades\Response;
use Psr\Http\Message\ResponseInterface;
use Hypervel\Support\Facades\Auth;
use App\Models\User;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Log;
use Hypervel\Support\Facades\Redis;
class LoginController
{
public function authenticate(Request $request): ResponseInterface
{
$credentials = $request->inputs(['mobile_number', 'password']);
$keepalive = $request->input('keepalive');
if (!$credentials['mobile_number'] || !$credentials['password']) {
return Response::json([
'success' => false,
'message' => 'Missing fields.',
], 422);
}
$candidates = self::phMobileVariants($credentials['mobile_number']);
$user = User::whereIn('mobile_number', $candidates)->first();
if (!$user) {
return Response::json([
'success' => false,
'message' => 'Account not found.',
], 401);
}
Log::info('Login attempt', [
'mobile_number' => $credentials['mobile_number'],
'candidates' => $candidates,
'user_found' => true,
'active' => $user->active,
'acct_type' => $user->acct_type->value ?? null,
]);
if (!$user->active) {
return Response::json([
'success' => false,
'message' => 'Account is inactive. Please contact support.',
], 401);
}
if ($user && Hash::check($credentials['password'], $user->password)) {
Auth::login($user); // or Auth::guard()->login($user)
$current_userHashkey = $user->hashkey;
$sessionId = session()->getId();
Redis::sadd("user_sessions:{$current_userHashkey}", $sessionId);
// $request->session()->regenerate();
Log::info('KeepAlive Value ' . $keepalive);
if ($keepalive === true || $keepalive === 'true') {
self::setSessiontoKeepAlive();
}
return Response::json([
'success' => true,
'message' => 'Login successful',
]);
}
return Response::json([
'success' => false,
'message' => 'Invalid credentials.',
], 401);
}
/**
* Set or extends current session auth<br>
* This Function is Not Working JWT Automatically sets based on config file ttl
* @param string|bool $sessionid if false then get current sessionID
* @param int|bool $aliveinseconds if true then consider it forever
* @return null|bool // Time To Live TTL
*/
public static function setSessiontoKeepAlive(string|false $sessionId = false, int|bool $aliveinseconds = true)
{
$sessionId = $sessionId ?: session()->getId();
if (!$sessionId) {
return false;
}
if ($aliveinseconds === true) {
$aliveinseconds = 7889472; // 3 months
}
if ($aliveinseconds === false) {
return false;
}
// The redis session driver stores the key under the Redis connection prefix.
// Cache::get/put uses a different prefix (cache store), so we use Redis::expire()
// directly to extend the TTL of the existing session key without reading its value.
$result = Redis::expire($sessionId, $aliveinseconds);
if (!$result) {
Log::warning('setSessiontoKeepAlive: session key not found in Redis, cannot extend TTL for: ' . $sessionId);
return false;
}
$ttl = Redis::ttl($sessionId);
Log::info('extended session: ' . $sessionId . ' for ' . $aliveinseconds . 's, TTL: ' . $ttl);
return $ttl;
}
/**
* Build all plausible stored forms of a Philippine mobile number so the
* caller can match whichever variant is in the DB. Variants returned:
* - 09XXXXXXXXX
* - 9XXXXXXXXX (no leading 0)
* - 639XXXXXXXXX
* - +639XXXXXXXXX
* plus the original raw input as a final fallback.
*/
public static function phMobileVariants(string $input): array
{
$digits = preg_replace('/\D+/', '', $input);
$core = null; // the 10-digit 9XXXXXXXXX portion
if (preg_match('/^639(\d{9})$/', $digits, $m)) {
$core = '9' . $m[1];
} elseif (preg_match('/^09(\d{9})$/', $digits, $m)) {
$core = '9' . $m[1];
} elseif (preg_match('/^9(\d{9})$/', $digits, $m)) {
$core = '9' . $m[1];
}
$variants = [$input];
if ($core) {
$variants[] = '0' . $core;
$variants[] = $core;
$variants[] = '63' . $core;
$variants[] = '+63' . $core;
}
return array_values(array_unique($variants));
}
/**
* Back-compat shim: still used by UserModifyAdminPageController to
* canonicalize on save (admin edit). Picks 09XXXXXXXXX when possible.
*/
public static function normalizePhMobile(string $input): string
{
$digits = preg_replace('/\D+/', '', $input);
if (preg_match('/^639(\d{9})$/', $digits, $m)) {
return '09' . $m[1];
}
if (preg_match('/^9(\d{9})$/', $digits, $m)) {
return '09' . $m[1];
}
if (preg_match('/^09\d{9}$/', $digits)) {
return $digits;
}
return $input;
}
public function extendcurrentSession()
{
$ttl = self::setSessiontoKeepAlive();
return Response::make('TTL is '.$ttl);
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Services\ActivityService;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Response;
use App\Http\Controllers\Helpers\ResponseHelper;
class ActivityController
{
/**
* Get recent activities.
*
* @param Request $request
* @return \Psr\Http\Message\ResponseInterface
*/
public function getRecent(Request $request)
{
$limit = (int) $request->input('limit', 10);
$service = new ActivityService();
$activities = $service->getRecentActivities($limit);
return Response::json([
'success' => true,
'data' => $activities
]);
}
/**
* Search activities.
*
* @param Request $request
* @return \Psr\Http\Message\ResponseInterface
*/
public function search(Request $request)
{
$query = $request->input('q', '');
$limit = (int) $request->input('limit', 20);
$service = new ActivityService();
if (empty($query)) {
$activities = $service->getRecentActivities($limit);
} else {
$activities = $service->searchActivities($query, $limit);
}
return Response::json([
'success' => true,
'data' => $activities
]);
}
}

View 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());
}
}
}

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Models\Market\Cart;
use App\Models\Market\CartItem;
use App\Models\Market\Product;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Str;
class CartController
{
public function getCart()
{
$user = Auth::user();
if (!$user) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$cart = Cart::firstOrCreate(['user_id' => $user->id]);
$items = $cart->items()->with('product')->get();
return Response::json([
'success' => true,
'cart' => $cart,
'items' => $items,
'total' => $items->sum(fn($item) => $item->price * $item->quantity)
]);
}
public function addItem(Request $request)
{
$user = Auth::user();
if (!$user) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$request->validate([
'product_hash' => 'required|string',
'quantity' => 'nullable|integer|min:1',
]);
$product = Product::where('hashkey', $request->input('product_hash'))->first();
if (!$product) {
return Response::json(['error' => 'Product not found'], 404);
}
$cart = Cart::firstOrCreate(['user_id' => $user->id]);
$item = $cart->items()->where('product_id', $product->id)->first();
if ($item) {
$item->quantity += $request->input('quantity', 1);
$item->save();
} else {
$cart->items()->create([
'product_id' => $product->id,
'quantity' => $request->input('quantity', 1),
'price' => $product->price,
'is_active' => true,
'hashkey' => Str::uuid()->toString(),
]);
}
return Response::json(['success' => true, 'message' => 'Item added to cart']);
}
public function updateItem(Request $request)
{
$user = Auth::user();
if (!$user) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$request->validate([
'item_hash' => 'required|string',
'quantity' => 'required|integer|min:1',
]);
$item = CartItem::where('hashkey', $request->input('item_hash'))->first();
if (!$item) {
return Response::json(['error' => 'Item not found'], 404);
}
// Verify cart ownership
$cart = Cart::find($item->cart_id);
if ($cart->user_id !== $user->id) {
return Response::json(['error' => 'Forbidden'], 403);
}
$item->quantity = $request->input('quantity');
$item->save();
return Response::json(['success' => true, 'message' => 'Cart updated']);
}
public function removeItem(Request $request)
{
$user = Auth::user();
if (!$user) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$request->validate([
'item_hash' => 'required|string',
]);
$item = CartItem::where('hashkey', $request->input('item_hash'))->first();
if (!$item) {
return Response::json(['error' => 'Item not found'], 404);
}
$cart = Cart::find($item->cart_id);
if ($cart->user_id !== $user->id) {
return Response::json(['error' => 'Forbidden'], 403);
}
$item->delete();
return Response::json(['success' => true, 'message' => 'Item removed from cart']);
}
public function clearCart()
{
$user = Auth::user();
if (!$user) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$cart = Cart::where('user_id', $user->id)->first();
if ($cart) {
$cart->items()->delete();
}
return Response::json(['success' => true, 'message' => 'Cart cleared']);
}
}

View File

@@ -0,0 +1,470 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\CooperativeMember;
use App\Models\Market\Organization;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Response;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use App\Enums\UserTypes;
use Hypervel\Support\Str;
class CooperativeController
{
public function listCooperatives(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$query = Organization::where('is_active', true)->where('type', 'COOPERATIVE');
$cooperatives = $query->withCount('members')->get();
return response()->json([
'success' => true,
'data' => $cooperatives
]);
}
public function getCooperative(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('hashkey');
if (!$hashkey) {
return ResponseHelper::returnIncorrectDetails();
}
$cooperative = Organization::where('hashkey', $hashkey)->with(['members.user.userInfo'])->first();
if (!$cooperative) {
return ResponseHelper::returnError('Cooperative not found', 404);
}
$currentUserMembership = null;
$user = Auth::user();
if ($user) {
$currentUserMembership = CooperativeMember::where('organization_id', $cooperative->id)
->where('user_id', $user->id)
->first();
}
return response()->json([
'success' => true,
'data' => $cooperative,
'is_member' => $currentUserMembership !== null,
'membership' => $currentUserMembership,
]);
}
public function createCooperative(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateOrganization)) {
return ResponseHelper::returnUnauthorized();
}
$name = $request->input('name');
$address = $request->input('address', '');
$registrationNumber = $request->input('registration_number', '');
$cin = $request->input('cin', '');
$tin = $request->input('tin', '');
$cooperativeType = $request->input('cooperative_type', '');
$cooperativeCategory = $request->input('cooperative_category', '');
$registrationDate = $request->input('registration_date', null);
$contactPerson = $request->input('contact_person', '');
$contactNumber = $request->input('contact_number', '');
$contactEmail = $request->input('contact_email', '');
if (empty(trim($name ?? ''))) {
return ResponseHelper::returnError('Cooperative name is required');
}
$cooperative = new Organization([
'hashkey' => Str::random(64),
'name' => trim($name),
'type' => 'COOPERATIVE',
'address' => trim($address ?? ''),
'registration_number' => trim($registrationNumber),
'cin' => trim($cin),
'tin' => trim($tin),
'cooperative_type' => trim($cooperativeType),
'cooperative_category' => trim($cooperativeCategory),
'registration_date' => $registrationDate,
'contact_person' => trim($contactPerson),
'contact_number' => trim($contactNumber),
'contact_email' => trim($contactEmail),
'is_active' => true,
'created_by' => Auth::id(),
]);
if ($cooperative->save()) {
return ResponseHelper::returnSuccessResponse($cooperative, $cooperative->hashkey, 'Cooperative created successfully');
}
return ResponseHelper::returnError('Failed to create cooperative');
}
public function joinCooperative(Request $request)
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
$cooperativeHash = $request->input('cooperative_hash');
if (!$cooperativeHash) {
return ResponseHelper::returnIncorrectDetails();
}
$cooperative = Organization::where('hashkey', $cooperativeHash)->first();
if (!$cooperative) {
return ResponseHelper::returnError('Cooperative not found', 404);
}
// Check if already a member
$existing = CooperativeMember::where('organization_id', $cooperative->id)
->where('user_id', $user->id)
->first();
if ($existing) {
return ResponseHelper::returnError('Already a member of this cooperative');
}
$memberFields = $request->only([
'role', 'membership_type', 'membership_level', 'officer_position', 'officer_level',
'concurrent_position', 'concurrent_level', 'cooperative_name_alt', 'cooperative_position', 'year_beginning',
'priority_sector', 'common_bond', 'vulnerability_classifications',
'philsys_id', 'sss_number', 'pagibig_number',
'slp_track', 'slp_association_name', 'listahanan_id', 'fourtps_household_id',
'tupad_category', 'tupad_insurance_beneficiary_name', 'tupad_insurance_beneficiary_relation',
'preferred_occupation', 'nsrp_skills', 'employment_status', 'program_participation',
]);
$member = new CooperativeMember(array_merge($memberFields, [
'organization_id' => $cooperative->id,
'user_id' => $user->id,
'role' => $request->input('role', 'MEMBER'),
'joined_at' => now(),
'is_active' => true,
]));
if ($member->save()) {
// Sync with user settings
$settings = $user->settings ?? [];
$cooperatives = $settings['cooperatives'] ?? [];
if (!in_array($cooperativeHash, $cooperatives)) {
$cooperatives[] = $cooperativeHash;
$settings['cooperatives'] = $cooperatives;
$user->settings = $settings;
$user->save();
}
return ResponseHelper::returnSuccessResponse($member, $member->hashkey, 'Successfully joined cooperative');
}
return ResponseHelper::returnError('Failed to join cooperative');
}
public function addMember(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$cooperativeHash = $request->input('cooperative_hash');
$userHash = $request->input('user_hash');
if (!$cooperativeHash || !$userHash) {
return ResponseHelper::returnIncorrectDetails();
}
$cooperative = Organization::where('hashkey', $cooperativeHash)->first();
$targetUser = User::where('hashkey', $userHash)->first();
if (!$cooperative || !$targetUser) {
return ResponseHelper::returnError('Cooperative or User not found', 404);
}
$existing = CooperativeMember::where('organization_id', $cooperative->id)
->where('user_id', $targetUser->id)
->first();
if ($existing) {
return ResponseHelper::returnError('User is already a member');
}
$memberFields = $request->only([
'role', 'membership_type', 'membership_level', 'officer_position', 'officer_level',
'concurrent_position', 'concurrent_level', 'cooperative_name_alt', 'cooperative_position', 'year_beginning',
'priority_sector', 'common_bond', 'vulnerability_classifications',
'philsys_id', 'sss_number', 'pagibig_number',
'slp_track', 'slp_association_name', 'listahanan_id', 'fourtps_household_id',
'tupad_category', 'tupad_insurance_beneficiary_name', 'tupad_insurance_beneficiary_relation',
'preferred_occupation', 'nsrp_skills', 'employment_status', 'program_participation',
]);
$member = new CooperativeMember(array_merge($memberFields, [
'organization_id' => $cooperative->id,
'user_id' => $targetUser->id,
'role' => $request->input('role', 'MEMBER'),
'joined_at' => now(),
'is_active' => true,
]));
if ($member->save()) {
return ResponseHelper::returnSuccessResponse($member, $member->hashkey, 'Member added to cooperative');
}
return ResponseHelper::returnError('Failed to add member');
}
public function updateMember(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$memberHash = $request->input('member_hash');
if (!$memberHash) {
return ResponseHelper::returnIncorrectDetails();
}
$member = CooperativeMember::where('hashkey', $memberHash)->first();
if (!$member) {
return ResponseHelper::returnError('Member record not found', 404);
}
$memberFields = $request->only([
'role', 'membership_type', 'membership_level', 'officer_position', 'officer_level',
'concurrent_position', 'concurrent_level', 'cooperative_name_alt', 'cooperative_position', 'year_beginning',
'is_active'
]);
$member->fill($memberFields);
if ($member->save()) {
return ResponseHelper::returnSuccessResponse($member, $member->hashkey, 'Membership details updated');
}
return ResponseHelper::returnError('Failed to update membership details');
}
public function registerMember(Request $request)
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
if (!UserPermissions::isActionPermitted($user->acct_type, UserActions::JoinCooperative)) {
return ResponseHelper::returnUnauthorized();
}
$cooperativeHash = $request->input('cooperative_hash');
if (!$cooperativeHash) {
return ResponseHelper::returnIncorrectDetails();
}
$cooperative = Organization::where('hashkey', $cooperativeHash)->first();
if (!$cooperative) {
return ResponseHelper::returnError('Cooperative not found', 404);
}
// Check if already a member
$existing = CooperativeMember::where('organization_id', $cooperative->id)
->where('user_id', $user->id)
->first();
if ($existing) {
return ResponseHelper::returnError('Already a member of this cooperative');
}
$memberFields = $request->only([
'role', 'membership_type', 'membership_level', 'officer_position', 'officer_level',
'concurrent_position', 'concurrent_level', 'cooperative_name_alt', 'cooperative_position', 'year_beginning',
'priority_sector', 'common_bond', 'vulnerability_classifications',
'philsys_id', 'sss_number', 'pagibig_number',
'slp_track', 'slp_association_name', 'listahanan_id', 'fourtps_household_id',
'tupad_category', 'tupad_insurance_beneficiary_name', 'tupad_insurance_beneficiary_relation',
'preferred_occupation', 'nsrp_skills', 'employment_status', 'program_participation',
]);
$member = new CooperativeMember(array_merge($memberFields, [
'hashkey' => Str::random(64),
'organization_id' => $cooperative->id,
'user_id' => $user->id,
'role' => $request->input('role', 'MEMBER'),
'joined_at' => now(),
'is_active' => true,
'created_by' => $user->id,
]));
if ($member->save()) {
// Sync with user settings
$settings = $user->settings ?? [];
$cooperatives = $settings['cooperatives'] ?? [];
if (!is_array($cooperatives)) $cooperatives = [];
if (!in_array($cooperativeHash, $cooperatives)) {
$cooperatives[] = $cooperativeHash;
$settings['cooperatives'] = $cooperatives;
$user->settings = $settings;
$user->save();
}
// Upgrade a plain USER to COOP_MEMBER on cooperative registration.
// Never downgrade a higher type (COORDINATOR, OPERATOR, etc.).
if ($user->acct_type === UserTypes::USER) {
$user->acct_type = UserTypes::COOP_MEMBER;
$user->save();
}
return ResponseHelper::returnSuccessResponse($member, $member->hashkey, 'Successfully registered as a cooperative member');
}
return ResponseHelper::returnError('Failed to register');
}
public function publicCompleteMembership(Request $request)
{
$userHashkey = $request->input('user_hashkey');
$coopHash = $request->input('cooperative_hash');
if (!$userHashkey || !$coopHash) {
return ResponseHelper::returnIncorrectDetails();
}
$user = User::where('hashkey', $userHashkey)->first();
if (!$user) {
return response()->json(['success' => false, 'message' => 'User not found'], 404);
}
$cooperative = Organization::where('hashkey', $coopHash)
->where('type', 'COOPERATIVE')
->where('is_active', true)
->first();
if (!$cooperative) {
return response()->json(['success' => false, 'message' => 'Cooperative not found'], 404);
}
$existing = CooperativeMember::where('organization_id', $cooperative->id)
->where('user_id', $user->id)
->first();
if ($existing) {
return response()->json(['success' => false, 'message' => 'Already a member'], 409);
}
$member = new CooperativeMember(array_merge(
$request->only([
'membership_type', 'membership_level', 'year_beginning',
'officer_position', 'officer_level', 'concurrent_position', 'concurrent_level',
'cooperative_position', 'cooperative_name_alt',
'priority_sector', 'common_bond', 'vulnerability_classifications',
'philsys_id', 'sss_number', 'pagibig_number',
'slp_track', 'slp_association_name', 'listahanan_id', 'fourtps_household_id',
'tupad_category', 'tupad_insurance_beneficiary_name', 'tupad_insurance_beneficiary_relation',
'preferred_occupation', 'nsrp_skills', 'employment_status', 'program_participation',
]),
[
'hashkey' => Str::random(64),
'organization_id' => $cooperative->id,
'user_id' => $user->id,
'role' => 'MEMBER',
'joined_at' => now(),
'is_active' => true,
'created_by' => $user->id,
]
));
$member->save();
$settings = $user->settings ?? [];
$settings['cooperatives'] = array_unique(array_merge($settings['cooperatives'] ?? [], [$coopHash]));
$user->settings = $settings;
$user->save();
return response()->json(['success' => true, 'message' => 'Membership application submitted successfully.']);
}
public function publicGetCooperative(Request $request, string $hkey)
{
try {
$cooperative = Organization::where('hashkey', $hkey)
->where('type', 'COOPERATIVE')
->where('is_active', true)
->select(['id', 'hashkey', 'name', 'type', 'cooperative_type', 'cooperative_category', 'contact_person', 'contact_number', 'address'])
->first();
} catch (\Throwable $e) {
return Response::json(['success' => false, 'message' => 'Service temporarily unavailable'], 500);
}
if (!$cooperative) {
return Response::json(['success' => false, 'message' => 'Cooperative not found'], 404);
}
return Response::json(['success' => true, 'data' => $cooperative]);
}
public function publicRegisterMember(Request $request)
{
$hkey = $request->input('cooperative_hash');
if (!$hkey) {
return ResponseHelper::returnIncorrectDetails();
}
$cooperative = Organization::where('hashkey', $hkey)
->where('type', 'COOPERATIVE')
->where('is_active', true)
->first();
if (!$cooperative) {
return Response::json(['success' => false, 'message' => 'Cooperative not found'], 404);
}
try {
$validated = $request->validate([
'name' => 'required|string|max:255',
'username' => 'required|string|max:255|unique:users,username',
'mobile_number' => ['required', 'string', 'max:20', 'unique:users,mobile_number', 'regex:/^(09|\+639)\d{9}$/'],
'password' => 'required|string|min:6',
]);
} catch (\Hypervel\Validation\ValidationException $e) {
return Response::json(['success' => false, 'errors' => $e->errors()], 422);
}
$parentUser = User::where('id', $cooperative->created_by)->first()
?? User::where('acct_type', 'COORDINATOR')->first()
?? User::orderBy('id')->first();
if (!$parentUser) {
return Response::json(['success' => false, 'message' => 'No valid parent user found'], 500);
}
$user = new User();
$user->username = $validated['username'];
$user->name = $validated['name'];
$user->mobile_number = $validated['mobile_number'];
$user->password = Hash::make($validated['password']);
$user->parentuid = $parentUser->id;
$user->acct_type = 'user';
$user->active = true;
$user->save();
return Response::json([
'success' => true,
'user_hashkey' => $user->hashkey,
'message' => 'Account created. Please complete your membership application.',
], 201);
}
}

View File

@@ -0,0 +1,257 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\FilesMainController;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\CooperativeDocument;
use App\Models\Market\Organization;
use App\Models\FileList;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use Hypervel\Support\Str;
class CooperativeDocumentController
{
public function listDocuments(Request $request)
{
$orgHash = $request->input('orgHash');
if (!$orgHash) {
return ResponseHelper::returnIncorrectDetails();
}
$org = Organization::where('hashkey', $orgHash)->first();
if (!$org) {
return ResponseHelper::returnError('Organization not found', 404);
}
// Get latest versions (where parent_hashkey IS NULL or it is the latest in its group)
// For simplicity, we'll fetch all active docs and group them by parent_hashkey or hashkey
$allDocs = CooperativeDocument::where('organization_id', $org->id)
->where('is_active', true)
->orderBy('version_number', 'desc')
->get();
$fileHashes = $allDocs->pluck('file_hashkey')->filter()->unique()->toArray();
$fileLists = FileList::whereIn('hashkey', $fileHashes)->with('fileContent')->get()->keyBy('hashkey');
$grouped = [];
foreach ($allDocs as $doc) {
$rootKey = $doc->parent_hashkey ?? $doc->hashkey;
if (!isset($grouped[$rootKey])) {
$grouped[$rootKey] = [];
}
$grouped[$rootKey][] = $doc;
}
$data = [];
foreach ($grouped as $rootKey => $versions) {
$latest = $versions[0]; // ordered desc by version_number
$fileList = $fileLists[$latest->file_hashkey] ?? null;
if ($fileList) {
$history = [];
foreach ($versions as $v) {
$vFileList = $fileLists[$v->file_hashkey] ?? null;
if ($vFileList) {
$history[] = [
'hashkey' => $v->hashkey,
'version' => $v->version_number,
'name' => $vFileList->filename,
'date' => $v->created_at ? $v->created_at->format('Y-m-d H:i') : null,
'note' => $v->revision_note,
'url' => $vFileList->resolvedUrl()
];
}
}
$data[] = [
'hashkey' => $latest->hashkey,
'parent_hashkey' => $latest->parent_hashkey,
'name' => $fileList->filename,
'type' => $latest->document_type ?? 'Document',
'date' => $latest->created_at ? $latest->created_at->toDateString() : null,
'size' => $this->formatBytes($fileList->fileContent->size_in_bytes ?? 0),
'url' => $fileList->resolvedUrl(),
'version' => $latest->version_number,
'note' => $latest->revision_note,
'history' => $history
];
}
}
return response()->json([
'success' => true,
'data' => $data
]);
}
public function uploadDocument(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$orgHash = $request->input('orgHash');
$docType = $request->input('type', 'OTHERS');
if (!$orgHash || !$request->hasFile('file')) {
return ResponseHelper::returnIncorrectDetails();
}
$org = Organization::where('hashkey', $orgHash)->first();
if (!$org) {
return ResponseHelper::returnError('Organization not found', 404);
}
$file = $request->file('file');
$filename = $file->getClientFilename();
$fileList = FilesMainController::uploadFileList(
$file,
$filename,
$filename,
'Cooperative Document for ' . $org->name,
['type' => 'coop_document', 'org_id' => $org->id],
'CooperativeDocuments',
[],
0,
'cooperative_document',
);
if (!$fileList || !isset($fileList->hashkey)) {
return ResponseHelper::returnError('File upload failed');
}
$doc = new CooperativeDocument([
'hashkey' => (string) Str::uuid(),
'organization_id' => $org->id,
'file_hashkey' => $fileList->hashkey,
'document_type' => $docType,
'created_by' => Auth::id(),
'is_active' => true,
]);
if ($doc->save()) {
return response()->json([
'success' => true,
'message' => 'Document uploaded successfully',
'data' => [
'hashkey' => $doc->hashkey,
'name' => $filename,
'url' => $fileList->resolvedUrl()
]
]);
}
return ResponseHelper::returnError('Failed to save document record');
}
public function reviseDocument(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$parentHash = $request->input('parentHash');
$note = $request->input('note');
if (!$parentHash || !$request->hasFile('file')) {
return ResponseHelper::returnIncorrectDetails();
}
$parentDoc = CooperativeDocument::where('hashkey', $parentHash)->first();
if (!$parentDoc) {
return ResponseHelper::returnError('Original document not found', 404);
}
// The real parent is either its own parent or it is the parent
$rootHash = $parentDoc->parent_hashkey ?? $parentDoc->hashkey;
// Find highest version number
$lastVersion = CooperativeDocument::where('hashkey', $rootHash)
->orWhere('parent_hashkey', $rootHash)
->max('version_number');
$file = $request->file('file');
$filename = $file->getClientFilename();
$fileList = FilesMainController::uploadFileList(
$file,
$filename,
$filename,
'Revision of ' . $parentDoc->hashkey,
['type' => 'coop_document_revision', 'parent_id' => $parentDoc->id],
'CooperativeDocuments',
[],
0,
'cooperative_document_revision',
);
if (!$fileList) {
return ResponseHelper::returnError('File upload failed');
}
$doc = new CooperativeDocument([
'hashkey' => (string) Str::uuid(),
'parent_hashkey' => $rootHash,
'version_number' => $lastVersion + 1,
'organization_id' => $parentDoc->organization_id,
'file_hashkey' => $fileList->hashkey,
'document_type' => $parentDoc->document_type,
'revision_note' => $note,
'created_by' => Auth::id(),
'is_active' => true,
]);
if ($doc->save()) {
return response()->json([
'success' => true,
'message' => 'Revision uploaded successfully',
'data' => $doc
]);
}
return ResponseHelper::returnError('Failed to save revision record');
}
public function deleteDocument(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('hashkey');
if (!$hashkey) {
return ResponseHelper::returnIncorrectDetails();
}
$doc = CooperativeDocument::where('hashkey', $hashkey)->first();
if (!$doc) {
return ResponseHelper::returnError('Document not found', 404);
}
// If it's a version, we might want to just deactivate that version.
// If it's the root, we might want to deactivate all versions.
// For now, let's just deactivate the specific one.
$doc->is_active = false;
if ($doc->save()) {
return response()->json(['success' => true, 'message' => 'Document/Revision deleted']);
}
return ResponseHelper::returnError('Failed to delete document');
}
private function formatBytes($bytes, $precision = 1)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= (1 << (10 * $pow));
return round($bytes, $precision) . ' ' . $units[$pow];
}
}

View File

@@ -0,0 +1,269 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Http\Controllers\Helpers\PaymentProcessor;
use App\Http\Controllers\Helpers\QrphDecoder;
use App\Models\SystemSetting;
use App\Models\Accounting\MemberLedger;
use App\Models\User;
use App\Models\GlobalTransaction;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\DB;
use Hypervel\Support\Str;
use App\Enums\Market\ProductTransactionType;
use App\Enums\Market\TransactionFlow;
class CreditController
{
public function getWalletData(Request $request)
{
$user = User::find(Auth::id());
if (!$user) return ResponseHelper::returnUnauthorized();
$history = MemberLedger::where('user_id', $user->id)
->where('is_active', true)
->orderBy('created_at', 'desc')
->limit(20)
->get();
return response()->json([
'success' => true,
'balance' => $user->total_balance,
'credit' => $user->total_credit,
'history' => $history
]);
}
public function topUp(Request $request)
{
// Check if Top Up is enabled globally
if (!(\App\Models\SystemSetting::getValue('top_up_enabled', true))) {
return ResponseHelper::returnError('Credit top-up is currently disabled by administrators.');
}
$amount = (float) $request->input('amount');
$method = $request->input('method', 'GCASH');
if ($amount <= 0) {
return ResponseHelper::returnError('Amount must be greater than zero');
}
$user = User::find(Auth::id());
if (!$user) return ResponseHelper::returnUnauthorized();
// Start Transaction
DB::beginTransaction();
try {
// 1. Simulate Payment Success (in real life this would be a webhook/callback)
$payment = PaymentProcessor::initiatePayment($amount, $method, $user->hashkey);
if (!$payment['success']) {
throw new \Exception('Payment initiation failed');
}
// 2. Update User Balance
$user->total_balance += $amount;
$user->save();
// 3. Record in MemberLedger
$ledger = new MemberLedger([
'hashkey' => Str::random(64),
'user_id' => $user->id,
'amount' => $amount,
'transaction_type' => 'TOP_UP',
'flow' => 'IN',
'balance_after' => $user->total_balance,
'description' => "Credit Top-up via {$method}",
'reference_id' => $payment['transaction_id'],
'created_by' => $user->id,
'is_active' => true,
]);
$ledger->save();
// 4. Record in GlobalTransaction (for compatibility with existing reports)
$globalTxn = new GlobalTransaction([
'hashkey' => Str::random(64),
'user_id' => $user->id,
'amount' => $amount,
'type' => ProductTransactionType::TOP_UP,
'status' => 'COMPLETED',
'description' => "Credit Top-up via {$method}",
'flow' => TransactionFlow::INCOME,
'created_by' => $user->id,
]);
$globalTxn->save();
DB::commit();
return response()->json([
'success' => true,
'message' => 'Top-up successful',
'balance' => $user->total_balance,
'transaction_id' => $payment['transaction_id']
]);
} catch (\Exception $e) {
DB::rollBack();
return ResponseHelper::returnError('Top-up failed: ' . $e->getMessage());
}
}
public function transferCredit(Request $request)
{
$recipientHash = $request->input('recipient_hash');
$amount = (float) $request->input('amount');
if ($amount <= 0) return ResponseHelper::returnError('Invalid amount');
$sender = User::find(Auth::id());
if (!$sender) return ResponseHelper::returnUnauthorized();
if ($sender->total_balance < $amount) {
return ResponseHelper::returnError('Insufficient balance');
}
$recipient = User::where('hashkey', $recipientHash)->first();
if (!$recipient) return ResponseHelper::returnError('Recipient not found');
if ($sender->id === $recipient->id) {
return ResponseHelper::returnError('Cannot transfer to yourself');
}
DB::beginTransaction();
try {
// Deduct from sender
$sender->total_balance -= $amount;
$sender->save();
// Add to recipient
$recipient->total_balance += $amount;
$recipient->save();
$txnRef = Str::random(12);
// Record Ledger for Sender
MemberLedger::create([
'hashkey' => Str::random(64),
'user_id' => $sender->id,
'amount' => $amount,
'transaction_type' => 'TRANSFER_OUT',
'flow' => 'OUT',
'balance_after' => $sender->total_balance,
'description' => "Credit Transfer to {$recipient->fullname}",
'reference_id' => $txnRef,
'created_by' => $sender->id,
]);
// Record Ledger for Recipient
MemberLedger::create([
'hashkey' => Str::random(64),
'user_id' => $recipient->id,
'amount' => $amount,
'transaction_type' => 'TRANSFER_IN',
'flow' => 'IN',
'balance_after' => $recipient->total_balance,
'description' => "Credit Transfer from {$sender->fullname}",
'reference_id' => $txnRef,
'created_by' => $sender->id,
]);
DB::commit();
return response()->json(['success' => true, 'message' => 'Transfer successful']);
} catch (\Exception $e) {
DB::rollBack();
return ResponseHelper::returnError('Transfer failed');
}
}
public function searchUsers(Request $request)
{
$query = $request->input('q');
if (empty($query)) return response()->json(['success' => true, 'data' => []]);
$users = User::where('fullname', 'like', "%{$query}%")
->orWhere('name', 'like', "%{$query}%")
->orWhere('mobile_number', 'like', "%{$query}%")
->where('id', '!=', Auth::id())
->limit(10)
->get(['hashkey', 'fullname', 'name', 'mobile_number']);
return response()->json(['success' => true, 'data' => $users]);
}
/**
* GET the current QRPH payment code (available to all authenticated users for top-up display).
*/
public function getQrphCode(Request $request)
{
$raw = SystemSetting::getValue('qrph_payment_code', null);
$imgHashkey = SystemSetting::getValue('qrph_payment_image_hashkey', null);
if (empty($raw)) {
return response()->json(['success' => true, 'qrph' => null, 'decoded' => null, 'image_url' => null]);
}
$decoded = QrphDecoder::decode($raw);
$imageUrl = $imgHashkey ? "/RequestData/File/{$imgHashkey}" : null;
return response()->json([
'success' => true,
'qrph' => $raw,
'decoded' => $decoded,
'image_url' => $imageUrl,
]);
}
/**
* SET the QRPH payment code — ULTIMATE only (enforced by route middleware).
* Accepts optional image_hashkey from a prior /File/Upload/QrphPayment call.
*/
public function setQrphCode(Request $request)
{
$raw = trim($request->input('qrph_code', ''));
$imgHashkey = trim($request->input('image_hashkey', ''));
if (empty($raw)) {
SystemSetting::setValue('qrph_payment_code', '');
SystemSetting::setValue('qrph_payment_image_hashkey', '');
return response()->json(['success' => true, 'message' => 'QRPH code cleared.']);
}
$decoded = QrphDecoder::decode($raw);
SystemSetting::setValue('qrph_payment_code', $raw);
if (!empty($imgHashkey)) {
SystemSetting::setValue('qrph_payment_image_hashkey', $imgHashkey);
}
$imageUrl = $imgHashkey ? "/RequestData/File/{$imgHashkey}" : null;
return response()->json([
'success' => true,
'message' => 'QRPH code saved.',
'decoded' => $decoded,
'image_url' => $imageUrl,
]);
}
/**
* Decode a QRPH string without saving — for preview before saving.
*/
public function decodeQrph(Request $request)
{
$raw = trim($request->input('qrph_code', ''));
if (empty($raw)) {
return ResponseHelper::returnError('No QR string provided.');
}
return response()->json([
'success' => true,
'decoded' => QrphDecoder::decode($raw),
]);
}
}

View File

@@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\FarmerProfile;
use App\Models\Market\Organization;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
class FarmerController
{
public function registerFarmer(Request $request)
{
$currentUser = Auth::user();
if (!$currentUser) {
return ResponseHelper::returnUnauthorized();
}
$validated = $request->validate([
'user_hash' => 'nullable|string',
'farm_name' => 'required|string|max:255',
'farm_location' => 'nullable|string',
'organization_hash' => 'nullable|string',
'main_crops' => 'nullable|array',
]);
// Determine the target user - if user_hash provided, use that user; otherwise use current user
if (!empty($validated['user_hash'])) {
$user = User::where('hashkey', $validated['user_hash'])->first();
if (!$user) {
return ResponseHelper::returnError('User not found', 404);
}
} else {
$user = $currentUser;
}
$organization = $validated['organization_hash'] ? Organization::where('hashkey', $validated['organization_hash'])->first() : null;
// Check if user already has a farmer profile
$existingProfile = FarmerProfile::where('user_id', $user->id)->first();
if ($existingProfile) {
return ResponseHelper::returnError('User already has a farmer profile');
}
$profile = new FarmerProfile([
'user_id' => $user->id,
'organization_id' => $organization?->id,
'farm_name' => $validated['farm_name'],
'farm_location' => $validated['farm_location'],
'main_crops' => $validated['main_crops'],
'verification_status' => 'VERIFIED',
'created_by' => $currentUser->id,
]);
if ($profile->save()) {
return ResponseHelper::returnSuccessResponse($profile, $profile->hashkey, 'Farmer profile created and pending verification');
}
return ResponseHelper::returnError('Failed to create farmer profile');
}
public function listFarmers(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewFarmers)) {
return ResponseHelper::returnUnauthorized();
}
$farmers = FarmerProfile::with(['user', 'organization'])->orderBy('created_at', 'desc')->get();
return response()->json([
'success' => true,
'data' => $farmers
]);
}
public function verifyFarmer(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::VerifyFarmer)) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('target');
$status = $request->input('status'); // VERIFIED, REJECTED
if (!$hashkey || !$status) {
return ResponseHelper::returnIncorrectDetails();
}
$profile = FarmerProfile::where('hashkey', $hashkey)->first();
if (!$profile) {
return ResponseHelper::returnError('Profile not found', 404);
}
$profile->verification_status = $status;
$profile->save();
return ResponseHelper::returnSuccessResponse($profile, $profile->hashkey, 'Farmer verification status updated');
}
public function listOrganizations()
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$orgs = Organization::where('is_active', true)->get();
return response()->json([
'success' => true,
'data' => $orgs
]);
}
public function createOrganization(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateOrganization)) {
return ResponseHelper::returnUnauthorized();
}
$validated = $request->validate([
'name' => 'required|string|max:255',
'type' => 'required|string|in:COOPERATIVE,ASSOCIATION,COMPANY',
'address' => 'nullable|string',
]);
$org = new Organization([
'name' => $validated['name'],
'type' => $validated['type'],
'address' => $validated['address'],
]);
if ($org->save()) {
return ResponseHelper::returnSuccessResponse($org, $org->hashkey, 'Organization created');
}
return ResponseHelper::returnError('Failed to create organization');
}
}

View File

@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\CooperativeResolution;
use App\Models\Market\CooperativeVote;
use App\Models\Market\Organization;
use App\Models\Market\CooperativeMember;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use Hypervel\Support\Str;
class GovernanceController
{
public function listResolutions(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewResolutions)) {
return ResponseHelper::returnUnauthorized();
}
$orgHash = $request->input('org_hash');
if (!$orgHash) {
return ResponseHelper::returnIncorrectDetails();
}
$org = Organization::where('hashkey', $orgHash)->first();
if (!$org) {
return ResponseHelper::returnError('Organization not found', 404);
}
$resolutions = CooperativeResolution::where('organization_id', $org->id)
->where('is_active', true)
->withCount(['votes as yes_votes' => function($query) {
$query->where('vote_cast', 'YES');
}])
->withCount(['votes as no_votes' => function($query) {
$query->where('vote_cast', 'NO');
}])
->withCount(['votes as abstain_votes' => function($query) {
$query->where('vote_cast', 'ABSTAIN');
}])
->orderBy('created_at', 'desc')
->get();
return response()->json([
'success' => true,
'data' => $resolutions
]);
}
public function createResolution(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateResolution)) {
return ResponseHelper::returnUnauthorized();
}
$orgHash = $request->input('org_hash');
$title = $request->input('title');
$description = $request->input('description');
if (!$orgHash || !$title) {
return ResponseHelper::returnIncorrectDetails();
}
$org = Organization::where('hashkey', $orgHash)->first();
if (!$org) {
return ResponseHelper::returnError('Organization not found', 404);
}
$resolution = new CooperativeResolution([
'hashkey' => Str::random(64),
'organization_id' => $org->id,
'title' => trim($title),
'description' => trim($description ?? ''),
'status' => 'PROPOSED',
'created_by' => Auth::id(),
'is_active' => true,
]);
if ($resolution->save()) {
return ResponseHelper::returnSuccessResponse($resolution, $resolution->hashkey, 'Resolution created successfully');
}
return ResponseHelper::returnError('Failed to create resolution');
}
public function castVote(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::VoteResolution)) {
return ResponseHelper::returnUnauthorized();
}
$resolutionHash = $request->input('resolution_hash');
$vote = $request->input('vote'); // YES, NO, ABSTAIN
if (!$resolutionHash || !in_array($vote, ['YES', 'NO', 'ABSTAIN'])) {
return ResponseHelper::returnIncorrectDetails();
}
$resolution = CooperativeResolution::where('hashkey', $resolutionHash)->first();
if (!$resolution) {
return ResponseHelper::returnError('Resolution not found', 404);
}
// Check if user is a member of the organization
$isMember = CooperativeMember::where('organization_id', $resolution->organization_id)
->where('user_id', Auth::id())
->where('is_active', true)
->exists();
if (!$isMember) {
return ResponseHelper::returnError('Only active members can vote on resolutions', 403);
}
// Check for existing vote
$existingVote = CooperativeVote::where('resolution_id', $resolution->id)
->where('user_id', Auth::id())
->first();
if ($existingVote) {
$existingVote->vote_cast = $vote;
$existingVote->updated_by = Auth::id();
if ($existingVote->save()) {
return ResponseHelper::returnSuccessResponse($existingVote, $existingVote->hashkey, 'Vote updated successfully');
}
} else {
$newVote = new CooperativeVote([
'hashkey' => Str::random(64),
'resolution_id' => $resolution->id,
'user_id' => Auth::id(),
'vote_cast' => $vote,
'created_by' => Auth::id(),
'is_active' => true,
]);
if ($newVote->save()) {
return ResponseHelper::returnSuccessResponse($newVote, $newVote->hashkey, 'Vote cast successfully');
}
}
return ResponseHelper::returnError('Failed to record vote');
}
public function updateResolutionStatus(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ManageOrganizations)) {
return ResponseHelper::returnUnauthorized();
}
$resolutionHash = $request->input('resolution_hash');
$status = $request->input('status'); // APPROVED, RESCINDED
if (!$resolutionHash || !in_array($status, ['APPROVED', 'RESCINDED', 'PROPOSED'])) {
return ResponseHelper::returnIncorrectDetails();
}
$resolution = CooperativeResolution::where('hashkey', $resolutionHash)->first();
if (!$resolution) {
return ResponseHelper::returnError('Resolution not found', 404);
}
$resolution->status = $status;
$resolution->updated_by = Auth::id();
if ($status === 'APPROVED') {
$resolution->date_approved = now();
}
if ($resolution->save()) {
return ResponseHelper::returnSuccessResponse($resolution, $resolution->hashkey, 'Resolution status updated');
}
return ResponseHelper::returnError('Failed to update resolution status');
}
}

View File

@@ -0,0 +1,472 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\PosSession;
use App\Models\Market\PosTransaction;
use App\Models\Market\Product;
use App\Models\Market\Store;
use App\Models\User;
use Hyperf\Stringable\Str;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\DB;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Response;
/**
* Performance / load-testing endpoints.
*
* These bypass session auth so a client machine can hit them with curl.
* Auth is via header `X-Perf-Token` matched against env `PERF_API_TOKEN`.
* If `PERF_API_TOKEN` is unset the endpoints are disabled (403 for all).
*
* All endpoints return timing metrics (ms) so the caller can chart how the
* box behaves under different batch sizes.
*/
class PerformanceController
{
private const DEFAULT_LIMIT = 1000;
private function authorize(Request $request)
{
$expected = (string) env('PERF_API_TOKEN', '');
if ($expected === '') {
return ResponseHelper::returnError('Performance API is disabled (PERF_API_TOKEN not set)', 403);
}
$provided = (string) ($request->header('X-Perf-Token') ?? $request->input('token', ''));
if (!hash_equals($expected, $provided)) {
return ResponseHelper::returnError('Invalid perf token', 401);
}
return null;
}
private function actingUser(): ?User
{
$hash = (string) env('PERF_ACTOR_HASH', '');
if ($hash !== '') {
$u = User::where('hashkey', $hash)->first();
if ($u) return $u;
}
return User::where('acct_type', UserTypes::ULTIMATE->value)
->orderBy('id', 'asc')
->first();
}
private function clampCount($raw): int
{
$n = (int) $raw;
if ($n < 1) $n = 1;
if ($n > self::DEFAULT_LIMIT) $n = self::DEFAULT_LIMIT;
return $n;
}
private function ms(float $start): float
{
return round((microtime(true) - $start) * 1000, 3);
}
public function ping(Request $request)
{
$deny = $this->authorize($request);
if ($deny) return $deny;
return Response::json([
'success' => true,
'ts' => now()->toIso8601String(),
'php' => PHP_VERSION,
]);
}
/**
* Core user seeder, returns ['hashes' => [...], 'ms' => float].
*/
private function _seedUsers(User $actor, int $count, string $type, ?int $parentId, string $prefix): array
{
$parentId = $parentId ?? $actor->id;
$hashed = Hash::make('Perf12345!');
$start = microtime(true);
$created = [];
DB::beginTransaction();
try {
foreach (range(1, $count) as $i) {
$suffix = Str::random(10);
$u = User::create([
'username' => "{$prefix}_{$suffix}",
'name' => "Perf User {$i}",
'mobile_number' => '09' . str_pad((string) random_int(0, 999999999), 9, '0', STR_PAD_LEFT),
'password' => $hashed,
'acct_type' => $type,
'parentuid' => $parentId,
'active' => true,
]);
$created[] = $u->hashkey;
}
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
return ['hashes' => $created, 'ms' => $this->ms($start)];
}
private function _seedStores(User $actor, int $count, string $category, ?int $ownerId, string $prefix): array
{
$start = microtime(true);
$created = [];
DB::beginTransaction();
try {
foreach (range(1, $count) as $i) {
$storeCode = StoreController::generateStoreCode($category);
$s = Store::create([
'storecode' => $storeCode,
'name' => "{$prefix} " . Str::random(8),
'description' => "Synthetic store for perf testing #{$i}",
'address' => 'Perf Lane ' . random_int(1, 9999),
'category' => $category,
'subcategory' => '',
'owner_id' => $ownerId,
'created_by' => $actor->id,
'is_active' => true,
'status' => 'active',
]);
$created[] = $s->hashkey;
}
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
return ['hashes' => $created, 'ms' => $this->ms($start)];
}
private function _seedProducts(User $actor, int $count, ?Store $store, bool $attach, string $prefix): array
{
$start = microtime(true);
$created = [];
DB::beginTransaction();
try {
foreach (range(1, $count) as $i) {
$price = random_int(10, 5000);
$available = random_int(10, 500);
$p = Product::create([
'name' => "{$prefix} " . Str::random(8),
'description' => "Synthetic product for perf testing #{$i}",
'price' => $price,
'unitname' => 'pc',
'available' => $available,
'category' => 'Perf',
'subcategory' => 'Synthetic',
'created_by' => $actor->id,
'is_active' => true,
]);
if ($store && $attach) {
$store->products()->attach($p->id, [
'available' => $available,
'price' => $price,
'description' => $p->description,
'is_active' => true,
]);
}
$created[] = $p->hashkey;
}
DB::commit();
} catch (\Throwable $e) {
DB::rollBack();
throw $e;
}
return ['hashes' => $created, 'ms' => $this->ms($start)];
}
/**
* POST /api/perf/seed/users
* body: { count, type?, parent_hash?, prefix? }
*/
public function seedUsers(Request $request)
{
$deny = $this->authorize($request);
if ($deny) return $deny;
$actor = $this->actingUser();
if (!$actor) return ResponseHelper::returnError('No actor user available', 500);
$count = $this->clampCount($request->input('count', 10));
$type = (string) $request->input('type', UserTypes::USER->value);
$prefix = (string) $request->input('prefix', 'perf');
if (!UserTypes::tryFrom($type)) {
return ResponseHelper::returnError("Invalid user type '{$type}'", 422);
}
$parentId = null;
if ($p = $request->input('parent_hash')) {
$parent = User::where('hashkey', (string) $p)->first();
if ($parent) $parentId = $parent->id;
}
try {
$r = $this->_seedUsers($actor, $count, $type, $parentId, $prefix);
} catch (\Throwable $e) {
return ResponseHelper::returnError($e->getMessage());
}
return Response::json([
'success' => true,
'count' => count($r['hashes']),
'total_ms' => $r['ms'],
'avg_ms' => round($r['ms'] / max(1, count($r['hashes'])), 3),
'sample' => array_slice($r['hashes'], 0, 5),
]);
}
/**
* POST /api/perf/seed/stores
* body: { count, owner_hash?, category?, prefix? }
*/
public function seedStores(Request $request)
{
$deny = $this->authorize($request);
if ($deny) return $deny;
$actor = $this->actingUser();
if (!$actor) return ResponseHelper::returnError('No actor user available', 500);
$count = $this->clampCount($request->input('count', 10));
$category = (string) $request->input('category', 'General');
$prefix = (string) $request->input('prefix', 'PerfStore');
$ownerId = null;
if ($h = $request->input('owner_hash')) {
$owner = User::where('hashkey', (string) $h)->first();
if ($owner) $ownerId = $owner->id;
}
try {
$r = $this->_seedStores($actor, $count, $category, $ownerId, $prefix);
} catch (\Throwable $e) {
return ResponseHelper::returnError($e->getMessage());
}
return Response::json([
'success' => true,
'count' => count($r['hashes']),
'total_ms' => $r['ms'],
'avg_ms' => round($r['ms'] / max(1, count($r['hashes'])), 3),
'sample' => array_slice($r['hashes'], 0, 5),
]);
}
/**
* POST /api/perf/seed/products
* body: { count, target_store_hash?, prefix?, attach_to_store? }
*/
public function seedProducts(Request $request)
{
$deny = $this->authorize($request);
if ($deny) return $deny;
$actor = $this->actingUser();
if (!$actor) return ResponseHelper::returnError('No actor user available', 500);
$count = $this->clampCount($request->input('count', 10));
$prefix = (string) $request->input('prefix', 'PerfProduct');
$attach = (bool) $request->input('attach_to_store', true);
$store = null;
if ($h = $request->input('target_store_hash')) {
$store = Store::where('hashkey', (string) $h)->first();
if (!$store) return ResponseHelper::returnError('Target store not found', 404);
}
try {
$r = $this->_seedProducts($actor, $count, $store, $attach, $prefix);
} catch (\Throwable $e) {
return ResponseHelper::returnError($e->getMessage());
}
return Response::json([
'success' => true,
'count' => count($r['hashes']),
'total_ms' => $r['ms'],
'avg_ms' => round($r['ms'] / max(1, count($r['hashes'])), 3),
'attached_to_store' => $store?->hashkey,
'sample' => array_slice($r['hashes'], 0, 5),
]);
}
/**
* POST /api/perf/seed/batch
* body: { users?, stores?, products?, prefix?, type?, category? }
* Runs all three seeders sequentially with per-phase timings.
*/
public function seedBatch(Request $request)
{
$deny = $this->authorize($request);
if ($deny) return $deny;
$actor = $this->actingUser();
if (!$actor) return ResponseHelper::returnError('No actor user available', 500);
$prefix = (string) $request->input('prefix', 'perf');
$type = (string) $request->input('type', UserTypes::USER->value);
if (!UserTypes::tryFrom($type)) {
return ResponseHelper::returnError("Invalid user type '{$type}'", 422);
}
$category = (string) $request->input('category', 'General');
$users = max(0, (int) $request->input('users', 0));
$stores = max(0, (int) $request->input('stores', 0));
$products = max(0, (int) $request->input('products', 0));
$phases = [];
$totalStart = microtime(true);
try {
if ($users > 0) {
$r = $this->_seedUsers($actor, $this->clampCount($users), $type, null, $prefix);
$phases['users'] = ['count' => count($r['hashes']), 'ms' => $r['ms']];
}
if ($stores > 0) {
$r = $this->_seedStores($actor, $this->clampCount($stores), $category, null, $prefix . 'Store');
$phases['stores'] = ['count' => count($r['hashes']), 'ms' => $r['ms']];
}
if ($products > 0) {
$r = $this->_seedProducts($actor, $this->clampCount($products), null, false, $prefix . 'Product');
$phases['products'] = ['count' => count($r['hashes']), 'ms' => $r['ms']];
}
} catch (\Throwable $e) {
return ResponseHelper::returnError($e->getMessage());
}
return Response::json([
'success' => true,
'total_ms' => $this->ms($totalStart),
'phases' => $phases,
]);
}
/**
* POST /api/perf/pos/simulate
* body: { store_hash, items?, cycles?, complete? }
*
* Runs end-to-end POS cycles entirely server-side: open session, add N
* line items, optionally complete + archive. Returns per-phase timings.
*/
public function simulatePos(Request $request)
{
$deny = $this->authorize($request);
if ($deny) return $deny;
$actor = $this->actingUser();
if (!$actor) return ResponseHelper::returnError('No actor user available', 500);
$storeHash = (string) $request->input('store_hash', '');
if ($storeHash === '') return ResponseHelper::returnError('store_hash is required', 422);
$store = Store::where('hashkey', $storeHash)->first();
if (!$store) return ResponseHelper::returnError('Store not found', 404);
$items = max(1, min(200, (int) $request->input('items', 5)));
$cycles = max(1, min(100, (int) $request->input('cycles', 1)));
$complete = (bool) $request->input('complete', true);
// Pull a pool of products attached to this store; fall back to global
// active products if the store has none yet.
$productIds = DB::table('prd_str')
->where('store_id', $store->id)
->where('is_active', true)
->pluck('product_id')
->toArray();
if (empty($productIds)) {
$productIds = Product::where('is_active', true)
->limit(max(50, $items * 2))
->pluck('id')
->toArray();
}
if (empty($productIds)) {
return ResponseHelper::returnError('No products available to simulate sales', 422);
}
$products = Product::whereIn('id', $productIds)->get(['id', 'hashkey', 'price'])->keyBy('id');
$cycleResults = [];
$totalStart = microtime(true);
for ($c = 1; $c <= $cycles; $c++) {
$cStart = microtime(true);
// 1. Open session
$t = microtime(true);
$session = PosSession::create([
'access_key' => Str::random(32),
'store_id' => $store->id,
'customer_name' => 'Perf Customer ' . $c,
'status' => 'active',
'created_by' => $actor->id,
]);
$openMs = $this->ms($t);
// 2. Add items (raw inserts for speed, mirrors PosController::addItem)
$t = microtime(true);
$now = now();
$rows = [];
$total = 0;
for ($i = 0; $i < $items; $i++) {
$pid = $productIds[array_rand($productIds)];
$product = $products[$pid] ?? null;
if (!$product) continue;
$qty = random_int(1, 5);
$price = (int) $product->price;
$line = $price * $qty;
$total += $line;
$rows[] = [
'pos_session_id' => $session->id,
'product_id' => $pid,
'quantity' => $qty,
'price_at_sale' => $price,
'total_price' => $line,
'hashkey' => Str::uuid()->toString() . Str::random(100),
'created_at' => $now,
'updated_at' => $now,
'created_by' => $actor->id,
];
}
if (!empty($rows)) {
DB::table('pos_transactions')->insert($rows);
}
$addMs = $this->ms($t);
// 3. Optionally complete the session
$completeMs = null;
if ($complete) {
$t = microtime(true);
DB::table('pos_sessions')->where('id', $session->id)->update([
'total_amount' => $total,
'received_amount' => $total,
'change_amount' => 0,
'status' => 'completed',
'payment_method' => 'cash',
'updated_at' => $now,
'updated_by' => $actor->id,
]);
$completeMs = $this->ms($t);
}
$cycleResults[] = [
'session_hash' => $session->hashkey,
'items' => count($rows),
'total' => $total,
'open_ms' => $openMs,
'add_items_ms' => $addMs,
'complete_ms' => $completeMs,
'cycle_ms' => $this->ms($cStart),
];
}
$totalMs = $this->ms($totalStart);
$cycleMsValues = array_column($cycleResults, 'cycle_ms');
$avg = $cycleMsValues ? array_sum($cycleMsValues) / count($cycleMsValues) : 0.0;
return Response::json([
'success' => true,
'store_hash' => $store->hashkey,
'cycles' => $cycles,
'items_per_cycle' => $items,
'total_ms' => $totalMs,
'avg_cycle_ms' => round($avg, 3),
'min_cycle_ms' => $cycleMsValues ? min($cycleMsValues) : 0,
'max_cycle_ms' => $cycleMsValues ? max($cycleMsValues) : 0,
'detail' => $cycleResults,
]);
}
}

View File

@@ -0,0 +1,774 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\PosSession;
use App\Models\Market\PosTransaction;
use App\Models\Market\PosSessionArchive;
use App\Models\Market\Product;
use App\Models\Market\Store;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use Hyperf\Stringable\Str;
use App\Models\Market\PosAccessKey;
use App\Models\Market\Customer;
use Hypervel\Support\Facades\DB;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use Hyperf\Coroutine\Coroutine;
use App\Http\Controllers\Helpers\CacheHelper;
class PosController
{
public function startSession(Request $request)
{
$validated = $request->validate([
'store_hash' => 'nullable|string',
'customer_name' => 'nullable|string|max:255',
'access_key' => 'nullable|string',
]);
$store = null;
$accessKeyObj = null;
if ($request->input('access_key')) {
$accessKeyObj = PosAccessKey::where('access_key', $request->input('access_key'))
->where('status', 'active')
->first();
if ($accessKeyObj) {
if ($accessKeyObj->isExpired()) {
$accessKeyObj->status = 'inactive';
$accessKeyObj->save();
return ResponseHelper::returnError('Access key has expired', 401);
}
$store = $accessKeyObj->store;
} elseif (!Auth::check()) {
return ResponseHelper::returnError('Invalid or inactive access key', 401);
}
}
if (!$store && !empty($validated['store_hash'])) {
$store = Store::where('hashkey', $validated['store_hash'])->first();
}
if (!$store) {
return ResponseHelper::returnError('No store found. Please open the POS from a store page or use a valid access key.', 422);
}
// Authorization check
if (!UserPermissions::isUserAllowedAccessToStore(Auth::user(), $store)) {
return ResponseHelper::returnError('You are not authorized to start a POS session for this store.', 403);
}
/** @var PosSession $session */
$session = PosSession::create([
'access_key' => $accessKeyObj ? $accessKeyObj->access_key : Str::random(32),
'store_id' => $store->id,
'customer_name' => $validated['customer_name'] ?? null,
'status' => 'active',
'created_by' => Auth::id(),
]);
if ($accessKeyObj) {
$accessKeyObj->last_used_at = now();
$accessKeyObj->save();
}
$this->archiveSession($session, 'Session initialized');
$this->invalidateSessionCache($session);
return ResponseHelper::returnSuccessResponse([
'hashkey' => $session->hashkey,
'access_key' => $session->access_key,
], $session->hashkey, 'POS Session started');
}
public function getSession(Request $request)
{
$hashkey = ResponseHelper::getTargetHash();
$accessKey = $request->input('access_key');
if (!$hashkey && !$accessKey) {
return ResponseHelper::returnError('No key provided', 400);
}
$session = null;
if ($hashkey) {
$q = $this->getBaseSessionQuery()->where('hashkey', $hashkey);
$session = CacheHelper::get_cache($q);
if (!$session) {
$session = $q->first();
if ($session) {
CacheHelper::set_cache($q, $session);
}
}
// If not a session hash, check if it's a store hash
if (!$session) {
$sq = Store::where('hashkey', $hashkey);
$store = CacheHelper::get_cache($sq);
if (!$store) {
$store = $sq->first();
if ($store) {
CacheHelper::set_cache($sq, $store);
}
}
if ($store) {
$q = $this->getBaseSessionQuery()
->where('store_id', $store->id)
->where('status', 'active')
->orderBy('id', 'desc');
$session = CacheHelper::get_cache($q);
if (!$session) {
$session = $q->first();
if ($session) {
CacheHelper::set_cache($q, $session);
}
}
}
}
}
// If still no session and we have an accessKey, try that (as fallback or primary if no hashkey)
if (!$session && $accessKey) {
$q = $this->getBaseSessionQuery()
->where('access_key', $accessKey)
->orderBy('id', 'desc');
$session = CacheHelper::get_cache($q);
if (!$session) {
$session = $q->first();
if ($session) {
CacheHelper::set_cache($q, $session);
}
}
}
if (!$session) {
return ResponseHelper::returnError('Session not found', 404);
}
// Authorization check
if (!UserPermissions::isUserAllowedAccessToStore(Auth::user(), $session->store_id)) {
return ResponseHelper::returnError('You are not authorized to access this POS session.', 403);
}
// Return the full session with all eager loaded relations
return ResponseHelper::returnSuccessResponse($session, $session->hashkey);
}
private function getBaseSessionQuery()
{
return PosSession::with([
'transactions.product' => function ($q) {
// Only fetch minimal columns needed for the POS to reduce serialization time
$q->select(['id', 'hashkey', 'name', 'price', 'photourl', 'unitname', 'category']);
},
'store'
]);
}
public function addItem(Request $request)
{
$validated = $request->validate([
'session_hash' => 'required|string',
'product_hash' => 'required|string',
'quantity' => 'required|integer|min:1',
'price' => 'nullable|numeric|min:0',
]);
$sq = PosSession::where('hashkey', $validated['session_hash']);
$session = CacheHelper::get_cache($sq);
if (!$session) {
$session = $sq->first();
if ($session) {
CacheHelper::set_cache($sq, $session);
}
}
$pq = Product::select(['id', 'hashkey', 'price', 'is_active', 'name'])->where('hashkey', $validated['product_hash']);
$product = CacheHelper::get_cache($pq);
if (!$product) {
$product = $pq->first();
if ($product) {
CacheHelper::set_cache($pq, $product);
}
}
if (!$session || !$product) {
return ResponseHelper::returnError('Session or Product not found', 404);
}
$sessionNeedsSave = false;
if ($session->status !== 'active') {
$session->status = 'active';
$sessionNeedsSave = true;
}
$price = (int) $product->price;
$isActive = $product->is_active;
if ($session->store_id) {
$psq = DB::table('prd_str')
->where('store_id', $session->store_id)
->where('product_id', $product->id)
->select('price', 'is_active');
$storeProduct = CacheHelper::get_cache($psq);
if (!$storeProduct) {
$storeProduct = $psq->first();
if ($storeProduct) {
CacheHelper::set_cache($psq, $storeProduct, [], 3600); // 1 hour
}
}
if ($storeProduct) {
if (isset($storeProduct->price)) {
$price = (int) $storeProduct->price;
}
if (isset($storeProduct->is_active)) {
$isActive = (bool) $storeProduct->is_active;
}
} else {
return ResponseHelper::returnError('Product not available in this store', 403);
}
}
if (!$isActive) {
return ResponseHelper::returnError('Product is currently inactive in this store', 403);
}
// Use custom price if provided, otherwise use calculated store/product price
if ($request->has('price')) {
$price = (int) $validated['price'];
}
// Update or create the transaction using raw DB for max speed
$existingTx = DB::table('pos_transactions')
->where('pos_session_id', $session->id)
->where('product_id', $product->id)
->first();
if ($existingTx) {
DB::table('pos_transactions')->where('id', $existingTx->id)->update([
'quantity' => $validated['quantity'],
'price_at_sale' => $price,
'total_price' => $price * $validated['quantity'],
'updated_at' => now(),
]);
} else {
DB::table('pos_transactions')->insert([
'pos_session_id' => $session->id,
'product_id' => $product->id,
'quantity' => $validated['quantity'],
'price_at_sale' => $price,
'total_price' => $price * $validated['quantity'],
'hashkey' => \Hyperf\Stringable\Str::uuid()->toString() . \Hyperf\Stringable\Str::random(100),
'created_at' => now(),
'updated_at' => now(),
'created_by' => Auth::id() ?? null,
]);
}
// Load specific columns to be fast, just like in getSession to reduce payload and memory
$session->load([
'transactions.product' => function ($q) {
$q->select(['id', 'hashkey', 'name', 'price', 'photourl', 'unitname', 'category']);
},
'store'
]);
$t_load = microtime(true);
// Update session totals in memory
$total = $session->transactions->where('is_void', false)->sum('total_price');
$updateData = [];
if ($session->total_amount !== (int) $total) {
$session->total_amount = (int) $total;
$updateData['total_amount'] = (int) $total;
}
if ($sessionNeedsSave) {
$updateData['status'] = $session->status;
}
// Use raw DB update to skip ModelSavingListener overhead while making sure we still record who updated it
if (!empty($updateData)) {
$updateData['updated_at'] = now();
$updateData['updated_by'] = Auth::id();
DB::table('pos_sessions')->where('id', $session->id)->update($updateData);
}
$t_db = microtime(true);
// Invalidate all possible session cache keys
$this->invalidateSessionCache($session);
// Archive the session using already loaded transaction data (deferred to background coroutine)
$this->archiveSession($session, 'Item added/updated: ' . $product->name, $session->transactions);
return ResponseHelper::returnSuccessResponse($session, $session->hashkey, 'Item added to session');
}
public function removeItem(Request $request)
{
$validated = $request->validate([
'session_hash' => 'required|string',
'transaction_id' => 'required|integer',
]);
$session = PosSession::where('hashkey', $validated['session_hash'])->first();
if (!$session) {
return ResponseHelper::returnError('Session not found', 404);
}
$transaction = PosTransaction::where('id', $validated['transaction_id'])
->where('pos_session_id', $session->id)
->first();
if ($transaction) {
$transaction->delete();
// Re-calculate and archive efficiently
// Load relations ONCE with only necessary columns
$session->load([
'transactions.product' => function ($q) {
$q->select(['id', 'hashkey', 'name', 'price', 'photourl', 'unitname', 'category']);
},
'store'
]);
$total = $session->transactions->where('is_void', false)->sum('total_price');
$session->total_amount = (int) $total;
DB::table('pos_sessions')->where('id', $session->id)->update([
'total_amount' => (int) $total,
'updated_at' => now(),
'updated_by' => Auth::id(),
]);
// Invalidate all possible session cache keys
$this->invalidateSessionCache($session);
$this->archiveSession($session, 'Item removed', $session->transactions);
} else {
$session->load([
'transactions.product' => function ($q) {
$q->select(['id', 'hashkey', 'name', 'price', 'photourl', 'unitname', 'category']);
},
'store'
]);
}
return ResponseHelper::returnSuccessResponse($session, $session->hashkey, 'Item removed from session');
}
public function completeSession(Request $request)
{
$validated = $request->validate([
'session_hash' => 'required|string',
'received_amount' => 'required|integer|min:0',
'payment_method' => 'required|string',
'payment_field' => 'nullable|string',
'customer_name' => 'nullable|string|max:255',
]);
$session = PosSession::where('hashkey', $validated['session_hash'])->first();
if (!$session) {
return ResponseHelper::returnError('Session not found', 404);
}
$session->received_amount = $validated['received_amount'];
$session->change_amount = $validated['received_amount'] - $session->total_amount;
$session->payment_method = $validated['payment_method'];
$session->payment_details = ['payment_field' => $validated['payment_field']];
if (!empty($validated['customer_name'])) {
$session->customer_name = $validated['customer_name'];
}
$session->status = 'completed';
$session->save();
// Invalidate cache
$this->invalidateSessionCache($session);
if (!empty($validated['customer_name'])) {
$customerName = trim($validated['customer_name']);
$customer = Customer::where('name', $customerName)
->where(function ($q) use ($session) {
$q->where('store_id', $session->store_id)
->orWhereNull('store_id');
})
->first();
if (!$customer) {
Customer::create([
'name' => $customerName,
'store_id' => $session->store_id,
'created_by' => Auth::id(),
]);
} else {
$customer->updated_at = now();
$customer->save();
}
}
$this->archiveSession($session, 'Session completed');
return ResponseHelper::returnSuccessResponse($session, $session->hashkey, 'Transaction completed');
}
public function syncOffline(Request $request)
{
$validated = $request->validate([
'transactions' => 'required|array',
'transactions.*.store_hash' => 'required|string',
'transactions.*.customer_name' => 'nullable|string',
'transactions.*.items' => 'required|array',
'transactions.*.total' => 'required|numeric',
'transactions.*.received' => 'required|numeric',
'transactions.*.method' => 'required|string',
'transactions.*.timestamp' => 'required|string',
'transactions.*.local_id' => 'nullable|integer',
]);
$syncedCount = 0;
$syncedIds = [];
$errors = [];
$affectedStoreIds = [];
foreach ($validated['transactions'] as $txn) {
try {
DB::beginTransaction();
$store = Store::where('hashkey', $txn['store_hash'])->first();
if (!$store) {
throw new \Exception('Store not found for hash: ' . $txn['store_hash']);
}
// Convert ISO 8601 timestamp to MySQL datetime format
$offlineTimestamp = date('Y-m-d H:i:s', strtotime($txn['timestamp']));
// Create the session
$session = new PosSession([
'store_id' => $store->id,
'customer_name' => $txn['customer_name'] ?? null,
'total_amount' => (int) $txn['total'],
'received_amount' => (int) $txn['received'],
'change_amount' => (int) ($txn['received'] - $txn['total']),
'payment_method' => $txn['method'],
'status' => 'completed',
'created_by' => Auth::id(),
'access_key' => 'synced-' . Str::random(32),
'hashkey' => Str::random(32) . '-' . Str::random(100),
]);
// Manually set timestamps to preserve offline time
$session->created_at = $offlineTimestamp;
$session->updated_at = $offlineTimestamp;
$session->save();
// Add Items
foreach ($txn['items'] as $item) {
$product = Product::where('hashkey', $item['product_hashkey'])->first();
if (!$product) continue;
DB::table('pos_transactions')->insert([
'pos_session_id' => $session->id,
'product_id' => $product->id,
'quantity' => $item['quantity'],
'price_at_sale' => (int) $item['price_at_sale'],
'total_price' => (int) ($item['price_at_sale'] * $item['quantity']),
'hashkey' => Str::uuid()->toString() . Str::random(100),
'created_at' => $offlineTimestamp,
'updated_at' => now(),
'created_by' => Auth::id(),
]);
}
// Handle Customer
if (!empty($txn['customer_name'])) {
$customerName = trim($txn['customer_name']);
$customer = Customer::where('name', $customerName)
->where(function ($q) use ($store) {
$q->where('store_id', $store->id)
->orWhereNull('store_id');
})
->first();
if (!$customer) {
Customer::create([
'name' => $customerName,
'store_id' => $store->id,
'created_by' => Auth::id(),
]);
}
}
$this->archiveSession($session, 'Offline synced transaction');
$this->invalidateSessionCache($session);
DB::commit();
$syncedCount++;
if (isset($txn['local_id'])) {
$syncedIds[] = $txn['local_id'];
}
if (!in_array($store->id, $affectedStoreIds)) {
$affectedStoreIds[] = $store->id;
}
} catch (\Exception $e) {
DB::rollBack();
$errors[] = $e->getMessage();
}
}
return ResponseHelper::returnSuccessResponse([
'synced_count' => $syncedCount,
'synced_ids' => $syncedIds,
'errors' => $errors
], 'sync_offline', "Synced $syncedCount transactions");
}
public function voidSession(Request $request)
{
$validated = $request->validate([
'session_hash' => 'required|string',
'remarks' => 'nullable|string',
]);
$session = PosSession::where('hashkey', $validated['session_hash'])->first();
if (!$session) {
return ResponseHelper::returnError('Session not found', 404);
}
if (!UserPermissions::isUserAllowedAccessToStore(Auth::user(), $session->store_id)) {
return ResponseHelper::returnError('You are not authorized to void this POS session.', 403);
}
$session->status = 'voided';
$session->is_void = true;
$session->save();
// Invalidate cache
$this->invalidateSessionCache($session);
$this->archiveSession($session, 'Session voided: ' . ($validated['remarks'] ?? 'No remarks'));
return ResponseHelper::returnSuccessResponse($session, $session->hashkey, 'Transaction voided');
}
public function getPosSessions(Request $request)
{
$validated = $request->validate([
'store_hash' => 'required|string',
'page' => 'nullable|integer|min:1',
'per_page' => 'nullable|integer|min:1|max:100',
]);
$store = Store::where('hashkey', $validated['store_hash'])->first();
if (!$store) {
return ResponseHelper::returnError('Store not found', 404);
}
// Authorization check
if (!UserPermissions::isUserAllowedAccessToStore(Auth::user(), $store)) {
return ResponseHelper::returnError('You are not authorized to view sessions for this store.', 403);
}
$page = (int) ($validated['page'] ?? 1);
$perPage = (int) ($validated['per_page'] ?? 10);
$offset = ($page - 1) * $perPage;
$query = PosSession::with(['transactions.product'])
->where('store_id', $store->id)
->where('status', '!=', 'active')
->orderBy('id', 'desc');
$totalCount = $query->count();
$sessions = $query->limit($perPage)->offset($offset)->get();
// Map sessions to include item count and simplify if needed
$sessions = $sessions->map(function ($session) {
return [
'hashkey' => $session->hashkey,
'status' => $session->status,
'total_amount' => $session->total_amount,
'customer_name' => $session->customer_name,
'payment_method' => $session->payment_method,
'items_count' => $session->transactions->count(),
'created_at' => $session->created_at,
'transactions' => $session->transactions,
];
});
return ResponseHelper::returnSuccessResponse([
'sessions' => $sessions,
'total_count' => $totalCount,
'page' => $page,
'per_page' => $perPage,
], $store->hashkey, 'POS sessions fetched');
}
public function getTodayStats(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewPosReports)) {
return ResponseHelper::returnUnauthorized();
}
$date = now()->format('Y-m-d');
$query = PosSession::where('status', 'completed')
->whereDate('created_at', $date);
$storeName = null;
$storePhoto = null;
// If store_hash is provided, filter by store
if ($request->input('store_hash')) {
$store = Store::where('hashkey', $request->input('store_hash'))->first();
if ($store) {
// Authorization check
if (!UserPermissions::isUserAllowedAccessToStore(Auth::user(), $store)) {
return ResponseHelper::returnError('You are not authorized to view reports for this store.', 403);
}
$query->where('store_id', $store->id);
$storeName = $store->name;
$storePhoto = $store->photourl;
}
}
$stats = CacheHelper::get_cache($query);
if ($stats) {
return ResponseHelper::returnSuccessResponse($stats, 'today_stats');
}
$stats = [
'count' => (int) $query->count(),
'total' => (int) $query->sum('total_amount'),
'store_name' => $storeName,
'store_photo' => $storePhoto,
];
CacheHelper::set_cache($query, $stats, [], 300); // 5 minutes
return ResponseHelper::returnSuccessResponse($stats, 'today_stats');
}
public function getCustomers(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewCustomers)) {
return ResponseHelper::returnUnauthorized();
}
$queryText = $request->input('query');
$storeHash = $request->input('store_hash');
$customerQuery = Customer::where('is_active', true);
if ($storeHash) {
$store = Store::where('hashkey', $storeHash)->first();
if ($store) {
// Authorization check
if (!UserPermissions::isUserAllowedAccessToStore(Auth::user(), $store)) {
return ResponseHelper::returnError('You are not authorized to view customers for this store.', 403);
}
$customerQuery->where(function ($q) use ($store) {
$q->where('store_id', $store->id)
->orWhereNull('store_id');
});
}
}
if ($queryText) {
$customerQuery->where('name', 'like', '%' . $queryText . '%');
}
$finalQuery = $customerQuery->orderBy('name', 'asc')->limit(10);
$customers = CacheHelper::get_cache($finalQuery);
if (!$customers) {
$customers = $finalQuery->get();
CacheHelper::set_cache($finalQuery, $customers, [], 3600); // 1 hour
}
return ResponseHelper::returnSuccessResponse($customers, 'customer_suggestions');
}
private function updateSessionTotals(PosSession $session)
{
$total = $session->transactions()->where('is_void', false)->sum('total_price');
$session->total_amount = (int) $total;
$session->save();
}
private function archiveSession(PosSession $session, string $remarks = '', $transactions = null)
{
// Serialize all data NOW before spawning coroutine to avoid context issues
$sessionData = $session->toArray();
$sessionId = $session->id;
$sessionHashkey = $session->hashkey;
$userId = Auth::id();
if ($transactions !== null) {
$transactionsData = $transactions->toArray();
} else {
$transactionsData = $session->transactions()->with('product')->get()->toArray();
}
// Defer the archive INSERT to a background coroutine so it doesn't block the response
Coroutine::create(function () use ($sessionId, $sessionHashkey, $sessionData, $transactionsData, $userId, $remarks) {
try {
PosSessionArchive::create([
'pos_session_id' => $sessionId,
'hashkey' => $sessionHashkey,
'session_snapshot' => $sessionData,
'transactions_snapshot' => $transactionsData,
'created_by' => $userId,
'remarks' => $remarks,
]);
} catch (\Throwable $e) {
// Silently fail — archive is non-critical audit data
}
});
}
private function invalidateSessionCache(PosSession $session)
{
// 1. Invalidate by hashkey (with relations)
CacheHelper::erase_cache($this->getBaseSessionQuery()->where('hashkey', $session->hashkey));
// 1b. Invalidate simple hashkey lookup (used in addItem)
CacheHelper::erase_cache(PosSession::where('hashkey', $session->hashkey));
// 2. Invalidate by store_id (last active session)
if ($session->store_id) {
CacheHelper::erase_cache($this->getBaseSessionQuery()
->where('store_id', $session->store_id)
->where('status', 'active')
->orderBy('id', 'desc'));
}
// 3. Invalidate by access_key
if ($session->access_key) {
CacheHelper::erase_cache($this->getBaseSessionQuery()
->where('access_key', $session->access_key)
->orderBy('id', 'desc'));
}
// 4. Invalidate today stats cache for the store
if ($session->store_id) {
$date = now()->format('Y-m-d');
$statsQuery = PosSession::where('status', 'completed')
->whereDate('created_at', $date)
->where('store_id', $session->store_id);
CacheHelper::erase_cache($statsQuery);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,190 @@
<?php
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Controller;
use App\Http\Controllers\FilesMainController;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
class ProductPhotoSearchController extends Controller
{
private const DDG_BROWSER_HEADERS = [
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Accept-Language: en-US,en;q=0.9',
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Referer: https://duckduckgo.com/',
];
// Step 1: fetch the DDG search page to extract the vqd session token.
// DDG requires this token to serve the image JSON endpoint.
private static function getDdgVqd(string $query): ?string
{
$url = 'https://duckduckgo.com/?q=' . urlencode($query) . '&iax=images&ia=images';
$ctx = stream_context_create(['http' => [
'method' => 'GET',
'header' => implode("\r\n", self::DDG_BROWSER_HEADERS),
'timeout' => 10,
]]);
$html = @file_get_contents($url, false, $ctx);
if (!$html) return null;
// The vqd token appears in the page JS in several formats depending on
// DDG's current build. Try the quoted forms first (vqd="4-xxx" /
// vqd='4-xxx' / vqd:"4-xxx"), then fall back to the bare form.
$patterns = [
'/vqd=["\']([0-9a-zA-Z._\-]+)["\']/', // vqd="4-123..." or vqd='4-123...'
'/vqd["\']?\s*[:=]\s*["\']([0-9a-zA-Z._\-]+)["\']/', // vqd:"4-123..."
'/vqd=([0-9a-zA-Z._\-]+)/', // bare vqd=4-123...
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $html, $m) && !empty($m[1])) {
return $m[1];
}
}
return null;
}
// GET /api/products/photo-search?q=...&page=1
public function search(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::SearchStockPhotos)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
$query = trim($request->input('q', ''));
$page = max(1, (int) $request->input('page', 1));
if (!$query) {
return response()->json(['error' => 'Query required'], 422);
}
$vqd = self::getDdgVqd($query);
if (!$vqd) {
return response()->json(['error' => 'Could not reach image search service'], 502);
}
// s = offset; DDG returns ~15 results per call; page 1 = s=0, page 2 = s=15, etc.
$offset = ($page - 1) * 15;
$url = 'https://duckduckgo.com/i.js?' . http_build_query([
'q' => $query,
'vqd' => $vqd,
'o' => 'json',
'p' => '1',
'f' => ',,,',
'l' => 'us-en',
's' => $offset,
]);
$ctx = stream_context_create(['http' => [
'method' => 'GET',
'header' => implode("\r\n", self::DDG_BROWSER_HEADERS),
'timeout' => 10,
]]);
$raw = @file_get_contents($url, false, $ctx);
if ($raw === false) {
return response()->json(['error' => 'Failed to fetch image results'], 502);
}
$data = json_decode($raw, true);
$results = $data['results'] ?? [];
$photos = array_map(fn($r) => [
'id' => md5($r['image']), // stable ID from image URL
'thumb' => $r['thumbnail'], // DDG-proxied small thumb (safe to display)
'src' => $r['image'], // actual source image URL (used for download)
'title' => $r['title'] ?? '',
], array_slice($results, 0, 15));
return response()->json([
'success' => true,
'photos' => $photos,
'page' => $page,
'has_more' => count($results) >= 15,
]);
}
// POST /api/products/photo-download
// body: { src: "https://..." } — the actual source image URL from DDG results
public function download(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::DownloadStockPhoto)) {
return response()->json(['error' => 'Unauthorized'], 403);
}
$src = $request->input('src', '');
// SSRF guard: must be http/https and must not target private/loopback IPs
$parsed = parse_url($src);
$scheme = $parsed['scheme'] ?? '';
$host = strtolower($parsed['host'] ?? '');
if (!in_array($scheme, ['http', 'https']) || !$host) {
return response()->json(['error' => 'Invalid URL'], 422);
}
// Block private/loopback ranges
if (preg_match('/^(localhost|127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|0\.0\.0\.0|::1)/i', $host)) {
return response()->json(['error' => 'Forbidden URL'], 403);
}
$ctx = stream_context_create(['http' => [
'method' => 'GET',
'header' => 'User-Agent: Mozilla/5.0' . "\r\n",
'timeout' => 15,
]]);
$raw = @file_get_contents($src, false, $ctx);
if ($raw === false || strlen($raw) < 500) {
return response()->json(['error' => 'Failed to fetch image'], 502);
}
// Resize to max 1280x720 using PHP GD (bundled — no Intervention Image needed)
$srcImg = @imagecreatefromstring($raw);
if (!$srcImg) {
return response()->json(['error' => 'Invalid image data'], 422);
}
$origW = imagesx($srcImg);
$origH = imagesy($srcImg);
$maxW = 1280;
$maxH = 720;
$ratio = min($maxW / $origW, $maxH / $origH, 1.0); // never upscale
$newW = (int) round($origW * $ratio);
$newH = (int) round($origH * $ratio);
$dstImg = imagescale($srcImg, $newW, $newH, IMG_BILINEAR_FIXED);
imagedestroy($srcImg);
ob_start();
imagejpeg($dstImg, null, 85);
$binary = ob_get_clean();
imagedestroy($dstImg);
// Save via existing pipeline — binary string branch in uploadFileContent handles this
$result = FilesMainController::uploadFileList(
$binary,
'stock-photo',
'stock_photo_' . time() . '.jpg',
'',
[],
'ProductMarket',
[],
0,
'image/jpeg'
);
if (!$result || empty($result->hashkey)) {
return response()->json(['error' => 'Save failed'], 500);
}
return response()->json([
'success' => true,
'hashkey' => $result->hashkey,
'url' => $result->resolvedUrl(),
]);
}
}

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\Courier;
use App\Models\Market\Shipment;
use App\Models\Market\Store;
use App\Models\Market\Customer;
use App\Models\GlobalTransaction;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
class ShipmentController
{
public function listShipments(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewShipments)) {
return ResponseHelper::returnUnauthorized();
}
$user = Auth::user();
$query = Shipment::with(['courier', 'transaction', 'store', 'customer']);
// filter by store if provided
if ($storeHash = $request->input('store_hash')) {
$store = Store::where('hashkey', $storeHash)->first();
if ($store) {
$query->where('store_id', $store->id);
}
}
// if not ultimate/admin, restrict to user's shipments
// (This logic might need adjustment based on how roles are defined)
// For now, let's just list all and allow filtering
$shipments = $query->orderBy('created_at', 'desc')->get();
return response()->json([
'success' => true,
'data' => $shipments
]);
}
public function createNewShipment(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateShipment)) {
return ResponseHelper::returnUnauthorized();
}
$user = Auth::user();
$validated = $request->validate([
'transaction_hash' => 'required|string',
'store_hash' => 'nullable|string',
'customer_hash' => 'nullable|string',
'courier_hash' => 'nullable|string',
'origin_address' => 'nullable|string',
'destination_address' => 'nullable|string',
'shipping_fee' => 'nullable|numeric',
'estimated_delivery_date' => 'nullable|date',
]);
$transaction = GlobalTransaction::where('hashkey', $validated['transaction_hash'])->first();
if (!$transaction) {
return ResponseHelper::returnError('Transaction not found', 404);
}
$store = $validated['store_hash'] ? Store::where('hashkey', $validated['store_hash'])->first() : null;
$customer = $validated['customer_hash'] ? Customer::where('hashkey', $validated['customer_hash'])->first() : null;
$courier = $validated['courier_hash'] ? Courier::where('hashkey', $validated['courier_hash'])->first() : null;
$shipment = new Shipment([
'transaction_id' => $transaction->id,
'store_id' => $store?->id,
'customer_id' => $customer?->id,
'courier_id' => $courier?->id,
'origin_address' => $validated['origin_address'] ?? $store?->address,
'destination_address' => $validated['destination_address'] ?? $customer?->address,
'shipping_fee' => $validated['shipping_fee'] ?? 0,
'estimated_delivery_date' => $validated['estimated_delivery_date'],
'status' => 'PENDING',
'created_by' => $user->id,
]);
if ($shipment->save()) {
return ResponseHelper::returnSuccessResponse($shipment, $shipment->hashkey, 'Shipment created successfully');
}
return ResponseHelper::returnError('Failed to create shipment');
}
public function updateShipmentStatus(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::UpdateShipmentStatus)) {
return ResponseHelper::returnUnauthorized();
}
$hashkey = $request->input('target');
$status = $request->input('status');
if (!$hashkey || !$status) {
return ResponseHelper::returnIncorrectDetails();
}
$shipment = Shipment::where('hashkey', $hashkey)->first();
if (!$shipment) {
return ResponseHelper::returnError('Shipment not found', 404);
}
$shipment->status = $status;
if ($status === 'DELIVERED') {
$shipment->actual_delivery_date = now();
}
$shipment->save();
return ResponseHelper::returnSuccessResponse($shipment, $shipment->hashkey, 'Shipment status updated');
}
public function listCouriers()
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewCouriers)) {
return ResponseHelper::returnUnauthorized();
}
$couriers = Courier::where('is_active', true)->get();
return response()->json([
'success' => true,
'data' => $couriers
]);
}
public function createCourier(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateCourier)) {
return ResponseHelper::returnUnauthorized();
}
$validated = $request->validate([
'name' => 'required|string|max:255',
'contact_number' => 'nullable|string',
'type' => 'required|string|in:INTERNAL,EXTERNAL',
]);
$courier = new Courier([
'name' => $validated['name'],
'contact_number' => $validated['contact_number'],
'type' => $validated['type'],
]);
if ($courier->save()) {
return ResponseHelper::returnSuccessResponse($courier, $courier->hashkey, 'Courier created');
}
return ResponseHelper::returnError('Failed to create courier');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,565 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\User;
use App\Models\Market\Store;
use App\Models\Market\Product;
use App\Models\GlobalTransaction;
use App\Models\FileContent;
use App\Models\DbBackup;
use Hyperf\Stringable\Str;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\DB;
use Hypervel\Support\Facades\Redis;
use Hypervel\Support\Facades\Response;
class UltimateController
{
/**
* Common check for Ultimate access.
*/
private function checkAccess()
{
if (!UserPermissions::isActionPermitted(0, UserActions::UltimateConsole)) {
return false;
}
return true;
}
/**
* Get system-wide statistics for the dashboard.
*/
public function getSystemStats()
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$globalMessage = Redis::get('system:global_message');
$redisStatus = ['connected' => false, 'ping_ms' => null, 'used_memory_human' => null, 'version' => null, 'error' => null];
try {
$start = microtime(true);
$pong = Redis::ping();
$redisStatus['ping_ms'] = round((microtime(true) - $start) * 1000, 2);
$redisStatus['connected'] = $pong === true || $pong === 'PONG' || $pong === '+PONG' || (is_string($pong) && stripos($pong, 'PONG') !== false);
$info = Redis::info();
if (is_array($info)) {
$flat = isset($info['Memory']) && is_array($info['Memory']) ? $info['Memory'] : $info;
$redisStatus['used_memory_human'] = $flat['used_memory_human'] ?? null;
$serverInfo = isset($info['Server']) && is_array($info['Server']) ? $info['Server'] : $info;
$redisStatus['version'] = $serverInfo['redis_version'] ?? null;
}
} catch (\Throwable $e) {
$redisStatus['error'] = $e->getMessage();
}
$stats = [
'users' => User::count(),
'active_users' => User::where('active', true)->count(),
'stores' => Store::count(),
'active_stores' => Store::where('is_active', true)->count(),
'products' => Product::count(),
'transactions' => GlobalTransaction::count(),
'total_balance' => GlobalTransaction::sum('amount'),
'php_version' => PHP_VERSION,
'server_time' => date('Y-m-d H:i:s'),
'maintenance_mode' => Redis::get('system:maintenance_mode') === 'true',
'global_message' => $globalMessage ? json_decode($globalMessage, true) : null,
'logs_count' => DB::table('logs')->count(),
'table_logs_count' => DB::table('table_logs')->count(),
'pos_sessions_count' => DB::table('pos_sessions')->count(),
'cooperatives_count' => DB::table('organizations')->where('type', 'COOPERATIVE')->count(),
'carts_count' => DB::table('carts')->count(),
'farmer_profiles_count' => DB::table('farmer_profiles')->count(),
'redis' => $redisStatus,
];
return Response::json(['success' => true, 'data' => $stats]);
}
/**
* Execute a raw SQL query.
*/
public function runQuery(Request $request)
{
if (Auth::user()->acct_type !== \App\Enums\UserTypes::ULTIMATE || !UserPermissions::isActionPermitted(0, UserActions::UltimateQuery)) {
return ResponseHelper::returnUnauthorized();
}
$query = $request->input('query');
if (empty($query)) return ResponseHelper::returnError('Query cannot be empty');
try {
$queryLower = strtolower(trim($query));
if (str_starts_with($queryLower, 'select') || str_starts_with($queryLower, 'show') || str_starts_with($queryLower, 'describe')) {
$results = DB::select($query);
return Response::json(['success' => true, 'data' => $results]);
} else {
$affected = DB::statement($query);
return Response::json(['success' => true, 'affected' => $affected]);
}
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Toggle maintenance mode system-wide.
*/
public function toggleMaintenance(Request $request)
{
if (!$this->checkAccess() || !UserPermissions::isActionPermitted(0, UserActions::UltimateMaintenance)) {
return ResponseHelper::returnUnauthorized();
}
$enabled = (bool) $request->input('enabled');
Redis::set('system:maintenance_mode', $enabled ? 'true' : 'false');
return Response::json(['success' => true, 'maintenance_mode' => $enabled]);
}
/**
* Send a global message / broadcast.
*/
public function sendGlobalMessage(Request $request)
{
if (!$this->checkAccess() || !UserPermissions::isActionPermitted(0, UserActions::UltimateGlobalMessage)) {
return ResponseHelper::returnUnauthorized();
}
$message = $request->input('message');
$type = $request->input('type', 'info'); // info, success, warning, danger
if (empty($message)) {
Redis::del('system:global_message');
return Response::json(['success' => true, 'message' => 'Global message cleared']);
}
Redis::set('system:global_message', json_encode([
'text' => $message,
'type' => $type,
'timestamp' => time()
]));
return Response::json(['success' => true]);
}
/**
* Flush / Truncate specific tables.
*/
public function flushData(Request $request)
{
if (!$this->checkAccess() || !UserPermissions::isActionPermitted(0, UserActions::UltimateFlush)) {
return ResponseHelper::returnUnauthorized();
}
$target = $request->input('target');
try {
$affected = 0;
switch ($target) {
case 'transactions':
$affected = GlobalTransaction::count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
GlobalTransaction::truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'pos_sessions':
$affected = DB::table('pos_sessions')->count();
DB::table('pos_sessions')->truncate();
break;
case 'cache':
Redis::flushDB();
break;
case 'stores':
$affected = DB::table('str')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('prd_str')->truncate();
DB::table('store_managers')->truncate();
DB::table('str')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'products':
$affected = DB::table('prd_items')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('prd_str')->truncate();
DB::table('prd_items')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'cooperatives':
$affected = DB::table('organizations')->where('type', 'COOPERATIVE')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('cooperative_votes')->truncate();
DB::table('cooperative_resolutions')->truncate();
DB::table('cooperative_documents')->truncate();
DB::table('cooperative_members')->truncate();
DB::table('organizations')->where('type', 'COOPERATIVE')->delete();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'carts':
$affected = DB::table('carts')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('cart_items')->truncate();
DB::table('carts')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
case 'farmer_profiles':
$affected = DB::table('farmer_profiles')->count();
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::table('farmer_profiles')->truncate();
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
break;
default:
return ResponseHelper::returnError('Invalid flush target');
}
return Response::json(['success' => true, 'message' => "Flushed $target successfully", 'affected' => $affected]);
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Trigger a test notification for a specific user.
*/
public function testNotification(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$userHash = $request->input('user_hash');
$user = User::where('hashkey', $userHash)->first();
if (!$user) return ResponseHelper::returnError('User not found');
// Setting exec_command which SSEController picks up to notify client
$user->exec_command = 'toast:success:Test Notification from Ultimate Console: ' . date('H:i:s');
$user->save();
return Response::json(['success' => true]);
}
/**
* Batch management for various entities.
*/
public function batchManage(Request $request)
{
if (!$this->checkAccess() || !UserPermissions::isActionPermitted(0, UserActions::UltimateBatch)) {
return ResponseHelper::returnUnauthorized();
}
$action = $request->input('action');
$ids = $request->input('ids', []);
$data = $request->input('data', []);
if (empty($ids) && !in_array($action, ['cleanup_sessions'])) {
return ResponseHelper::returnError('No IDs provided');
}
try {
switch($action) {
case 'activate_users':
User::whereIn('id', $ids)->update(['active' => true]);
break;
case 'deactivate_users':
User::whereIn('id', $ids)->update(['active' => false]);
break;
case 'cleanup_sessions':
DB::table('pos_sessions')->where('status', 'VOIDED')->delete();
break;
case 'mass_transfer_points':
$amount = (float)($data['amount'] ?? 0);
if ($amount <= 0) return ResponseHelper::returnError('Invalid amount');
foreach ($ids as $id) {
GlobalTransaction::create([
'user_id' => $id,
'amount' => $amount,
'type' => 'REWARD',
'description' => 'Mass points adjustment via Ultimate Console',
'is_active' => true,
]);
}
break;
default:
return ResponseHelper::returnError('Invalid batch action');
}
return Response::json(['success' => true]);
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Run a system command (Artisan wrapper).
*/
public function runCommand(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$command = $request->input('command');
if (empty($command)) return ResponseHelper::returnError('Command cannot be empty');
// Normalize command: strip 'php artisan ' if present
$command = preg_replace('/^php artisan\s+/', '', trim($command));
// Mapping for user-friendly commands
if ($command === 'reset-app all users') {
$command = 'app:reset-users';
}
if ($command === 'db seed') {
$command = 'db:seed';
}
// For security, only allow specific commands
$allowedCommands = [
'cache:clear', 'view:clear', 'config:clear', 'route:clear',
'migrate', 'migrate:rollback', 'migrate:fresh',
'db:seed', 'app:reset-users', 'optimize', 'optimize:clear'
];
$baseCommand = explode(' ', trim($command))[0];
if (!in_array($baseCommand, $allowedCommands)) {
return ResponseHelper::returnError("Command '{$baseCommand}' not allowed for security reasons.");
}
try {
// In Hyperf, running commands from HTTP request context is tricky.
// We'll use shell_exec in this local environment demo as a fallback.
$output = shell_exec("php artisan $command 2>&1");
return Response::json(['success' => true, 'output' => $output]);
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Run `php artisan migrate --force` (non-interactive).
*/
public function runMigrate(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
try {
$output = shell_exec('cd ' . escapeshellarg(BASE_PATH) . ' && php artisan migrate --force 2>&1');
return Response::json(['success' => true, 'output' => $output]);
} catch (\Throwable $th) {
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Download a full database backup.
* Puts system in maintenance mode during the process.
*/
public function downloadBackup()
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
// 1. Enable maintenance mode & Notify
Redis::set('system:maintenance_mode', 'true');
Redis::set('system:global_message', json_encode(['text' => 'System backup in progress. Transactions temporarily disabled.', 'type' => 'warning']));
try {
$filename = 'backup_' . date('Y-m-d_H-i-s') . '.sql';
$path = BASE_PATH . '/storage/app/backups/' . $filename;
if (!is_dir(dirname($path))) {
mkdir(dirname($path), 0755, true);
}
$dbHost = env('DB_HOST', '127.0.0.1');
$dbPort = env('DB_PORT', '3306');
$dbName = env('DB_DATABASE', 'bukid');
$dbUser = env('DB_USERNAME', 'root');
$dbPass = env('DB_PASSWORD', '');
$dump = new \Ifsnop\Mysqldump\Mysqldump(
"mysql:host={$dbHost};port={$dbPort};dbname={$dbName}",
$dbUser,
$dbPass,
[
'add-drop-table' => true,
'exclude-tables' => ['db_backups'] // Exclude the backups table
]
);
$dump->start($path);
if (!file_exists($path) || filesize($path) === 0) {
throw new \Exception('Backup file was not created or is empty.');
}
// Compress into 7z Ultra
$sevenZFilename = 'backup_' . date('Y-m-d_H-i-s') . '.7z';
$sevenZPath = BASE_PATH . '/storage/app/backups/' . $sevenZFilename;
// -mx=9 for Ultra compression
$path_escaped = escapeshellarg($path);
$sevenZPath_escaped = escapeshellarg($sevenZPath);
$command = "7z a -t7z -m0=lzma2 -mx=9 {$sevenZPath_escaped} {$path_escaped} 2>&1";
shell_exec($command);
if (!file_exists($sevenZPath)) {
throw new \Exception('Failed to create 7z archive.');
}
// Save to database
$fileContentRaw = file_get_contents($sevenZPath);
$fileHash = hash('sha256', $fileContentRaw);
$fileContent = new FileContent();
$fileContent->filehash = $fileHash;
$fileContent->titlename = $sevenZFilename;
$fileContent->description = 'System database backup';
$fileContent->size_in_bytes = filesize($sevenZPath);
$fileContent->content = base64_encode($fileContentRaw);
$fileContent->mimetype = 'application/x-7z-compressed';
$fileContent->created_by = Auth::id();
$fileContent->updated_by = Auth::id();
$fileContent->save();
$dbBackup = new DbBackup();
$dbBackup->file_content_hashkey = $fileContent->hashkey;
$dbBackup->filename = $sevenZFilename;
$dbBackup->size_in_bytes = filesize($sevenZPath);
$dbBackup->created_by = Auth::id();
$dbBackup->updated_by = Auth::id();
$dbBackup->save();
// Clean up the temporary files from filesystem
@unlink($path);
@unlink($sevenZPath);
// 2. Disable maintenance mode & Clear Notify
Redis::set('system:maintenance_mode', 'false');
Redis::del('system:global_message');
// 3. Return the binary content for download
return Response::make($fileContentRaw, 200, [
'Content-Type' => 'application/x-7z-compressed',
'Content-Disposition' => 'attachment; filename="' . $sevenZFilename . '"',
'Content-Length' => strlen($fileContentRaw),
]);
} catch (\Throwable $th) {
Redis::set('system:maintenance_mode', 'false');
return ResponseHelper::returnError($th->getMessage());
}
}
/**
* Get recently created backups.
*/
public function getBackups()
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$backups = DbBackup::with(['creator'])
->orderBy('created_at', 'desc')
->limit(50)
->get();
return Response::json(['success' => true, 'data' => $backups]);
}
/**
* Rename a specific backup.
*/
public function renameBackup(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$hash = $request->input('hash');
$newName = $request->input('name');
if (empty($newName)) return ResponseHelper::returnError('Name cannot be empty');
$backup = DbBackup::where('hashkey', $hash)->first();
if (!$backup) return ResponseHelper::returnError('Backup not found');
$backup->name = $newName;
$backup->save();
return Response::json(['success' => true]);
}
/**
* Delete a specific backup.
*/
public function deleteBackup(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$hash = $request->input('hash');
$backup = DbBackup::where('hashkey', $hash)->first();
if (!$backup) return ResponseHelper::returnError('Backup not found');
// Delete associated file content
FileContent::where('hashkey', $backup->file_content_hashkey)->delete();
// Delete backup record
$backup->delete();
return Response::json(['success' => true]);
}
/**
* Download a specific backup from the database.
*/
public function downloadBackupByHash(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$hash = $request->input('hash');
$backup = DbBackup::where('hashkey', $hash)->first();
if (!$backup) return ResponseHelper::returnError('Backup not found');
$fileContent = FileContent::where('hashkey', $backup->file_content_hashkey)->first();
if (!$fileContent) return ResponseHelper::returnError('File content not found');
$content = base64_decode($fileContent->content);
return Response::make($content, 200, [
'Content-Type' => $fileContent->mimetype,
'Content-Disposition' => 'attachment; filename="' . $backup->filename . '"',
'Content-Length' => strlen($content),
]);
}
/**
* Get system-wide logs from file and database.
*/
public function getSystemLogs(Request $request)
{
if (!$this->checkAccess()) return ResponseHelper::returnUnauthorized();
$type = $request->input('type', 'database');
if ($type === 'file') {
$logPath = BASE_PATH . '/storage/logs/hypervel.log';
if (!file_exists($logPath)) {
return Response::json(['success' => true, 'data' => 'No file logs found.']);
}
$logs = shell_exec("tail -n 1000 " . escapeshellarg($logPath));
return Response::json(['success' => true, 'data' => $logs]);
}
// Database logs (audit)
if ($type === 'audit') {
$logs = DB::table('table_logs')->orderBy('id', 'desc')->limit(500)->get();
return Response::json(['success' => true, 'data' => $logs]);
}
// Database logs (system)
$logs = DB::table('logs')->orderBy('uid', 'desc')->limit(500)->get();
return Response::json(['success' => true, 'data' => $logs]);
}
}

View File

@@ -0,0 +1,169 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Market\UserInfo;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
class UserInfoController
{
public function getUserInfo(Request $request, string $hashkey)
{
$targetUser = User::where('hashkey', $hashkey)->first();
if (!$targetUser) {
return ResponseHelper::returnError('User not found', 404);
}
$currentUser = Auth::user();
if (!$currentUser) {
return ResponseHelper::returnUnauthorized();
}
// Check permission: can view self or has ViewUserInfo permission for others
if ($currentUser->id !== $targetUser->id && !UserPermissions::isActionPermitted($currentUser->acct_type, UserActions::ViewUserInfo)) {
return ResponseHelper::returnUnauthorized();
}
$userInfo = $targetUser->userInfo;
if (!$userInfo) {
// Lazy create if it doesn't exist (should have been backfilled but just in case)
$userInfo = UserInfo::create([
'user_id' => $targetUser->id,
'fullname' => $targetUser->fullname ?? $targetUser->name,
'email' => $targetUser->email,
'mobile' => $targetUser->mobile_number,
'is_active' => true,
]);
}
return response()->json([
'success' => true,
'data' => $userInfo
]);
}
public function updateUserInfo(Request $request, string $hashkey)
{
$targetUser = User::where('hashkey', $hashkey)->first();
if (!$targetUser) {
return ResponseHelper::returnError('User not found', 404);
}
$currentUser = Auth::user();
if (!$currentUser) {
return ResponseHelper::returnUnauthorized();
}
// Check permission: can manage self or has ManageUserInfo permission for others
if ($currentUser->id !== $targetUser->id && !UserPermissions::isActionPermitted($currentUser->acct_type, UserActions::ManageUserInfo)) {
return ResponseHelper::returnUnauthorized();
}
$userInfo = $targetUser->userInfo;
if (!$userInfo) {
$userInfo = new UserInfo(['user_id' => $targetUser->id]);
}
$validated = $request->validate([
'firstname' => 'nullable|string|max:255',
'middlename' => 'nullable|string|max:255',
'lastname' => 'nullable|string|max:255',
'suffix' => 'nullable|string|max:50',
'gender' => 'nullable|string|max:50',
'dob' => 'nullable|date',
'priority_sector' => 'nullable|string|max:255',
'messenger_id' => 'nullable|string|max:255',
'viber_number' => 'nullable|string|max:255',
'tiktok_username' => 'nullable|string|max:255',
'region' => 'nullable|string|max:255',
'province' => 'nullable|string|max:255',
'city' => 'nullable|string|max:255',
'barangay' => 'nullable|string|max:255',
'civil_status' => 'nullable|string|max:100',
'children_count' => 'nullable|integer',
'dependent_count' => 'nullable|integer',
'education_level' => 'nullable|string|max:255',
'course' => 'nullable|string|max:255',
'school' => 'nullable|string|max:255',
'year_last_attended' => 'nullable|string|max:50',
'livelihood_source' => 'nullable|string|max:255',
'last_company' => 'nullable|string|max:255',
'employer_name' => 'nullable|string|max:255',
'last_position' => 'nullable|string|max:255',
'occupation' => 'nullable|string|max:255',
'last_employment_year' => 'nullable|string|max:50',
'monthly_income' => 'nullable|numeric',
'tin' => 'nullable|string|max:100',
'philhealth_id' => 'nullable|string|max:100',
'gov_id' => 'nullable|string|max:100',
'id_type' => 'nullable|string|max:100',
'id_number' => 'nullable|string|max:100',
'beneficiary_type' => 'nullable|string|max:100',
'emergency_contact_name' => 'nullable|string|max:255',
'emergency_contact_address' => 'nullable|string|max:255',
'emergency_contact_phone' => 'nullable|string|max:50',
'emergency_contact_relation' => 'nullable|string|max:100',
'emergency_contact_user_id' => 'nullable|integer',
'fullname' => 'nullable|string|max:255',
'landline' => 'nullable|string|max:20',
'mobile' => 'nullable|string|max:20',
'email' => 'nullable|email|max:255',
'alt_email' => 'nullable|email|max:255',
'alt_landline' => 'nullable|string|max:20',
'alt_mobile' => 'nullable|string|max:20',
'facebook_url' => 'nullable|url|max:255',
'bank_details' => 'nullable|array',
'bank_account_no' => 'nullable|string|max:100',
'addresses' => 'nullable|array',
'other_details' => 'nullable|array',
]);
// Logic to automatically populate emergency_contact_user_id if phone matches a registered user
if (!empty($validated['emergency_contact_phone'])) {
$matchedUser = User::where('mobile_number', $validated['emergency_contact_phone'])->first();
if ($matchedUser) {
$validated['emergency_contact_user_id'] = $matchedUser->id;
}
}
$userInfo->fill($validated);
if ($userInfo->save()) {
// Also update core user fields if they match
if (isset($validated['fullname'])) $targetUser->fullname = $validated['fullname'];
if (isset($validated['email'])) $targetUser->email = $validated['email'];
if (isset($validated['mobile'])) $targetUser->mobile_number = $validated['mobile'];
$targetUser->save();
return ResponseHelper::returnSuccessResponse($userInfo, $userInfo->hashkey, 'User info updated');
}
return ResponseHelper::returnError('Failed to update user info');
}
public function searchEmergencyContact(Request $request)
{
$query = $request->input('q');
if (empty($query)) {
return response()->json(['success' => true, 'data' => []]);
}
$users = User::where('name', 'like', "%$query%")
->orWhere('fullname', 'like', "%$query%")
->orWhere('mobile_number', 'like', "%$query%")
->limit(10)
->get(['id', 'name', 'fullname', 'mobile_number', 'hashkey']);
return response()->json([
'success' => true,
'data' => $users
]);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Market;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\User;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
class UserSettingsController
{
/**
* Get the current user's settings.
*/
public function getSettings()
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
return response()->json($user->settings ?? []);
}
/**
* Update the current user's settings.
*/
public function updateSettings(Request $request)
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
$newSettings = $request->all();
$currentSettings = $user->settings ?? [];
// Merge new settings into current settings
$updatedSettings = array_merge($currentSettings, $newSettings);
// Save to database
$user->settings = $updatedSettings;
$user->save();
return response()->json([
'success' => true,
'settings' => $user->settings
]);
}
}

View File

@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Facades\File;
use Hypervel\Support\Facades\Cache;
class PageMemoryController
{
public static function getPageInMemory(string $viewName, array $data = [], int $ttlSeconds = 100)
{
$dataHash = sha1(json_encode($data));
$cacheKey = 'Cache:Pages:Views:' . $viewName . '-------' . $dataHash;
return Response::html(
cache()->remember($cacheKey, $ttlSeconds, function () use ($viewName, $data) {
return view($viewName, $data)->render();
})
);
}
public static function readAssetInMemory(string $assetFilename, int $ttlSeconds = 10000, $asset_folder = null): ?string
{
$assetFilename = ltrim($assetFilename, '/\\');
$cacheKey = 'Cache:Assets:Static:' . $assetFilename;
if (!$asset_folder) {
$filePath = storage_path('app/cache/static/' . $assetFilename);
} else {
$filePath = $asset_folder . $assetFilename;
}
$data = Cache::get($cacheKey);
if ($data) {
return $data['content'];
}
if (!File::exists($filePath)) {
return null;
}
$fileContent = file_get_contents($filePath);
$mimeType = File::mimeType($filePath) ?? 'application/octet-stream';
$data = [
'content' => $fileContent,
'mime' => $mimeType,
];
Cache::put($cacheKey, $data, $ttlSeconds);
return $fileContent;
}
public static function readPublicAssetInMemory($assetFilename, $ttlSeconds = 10000)
{
$asset_folder = public_path('static/');
return self::readAssetInMemory($assetFilename, $ttlSeconds, $asset_folder);
}
public static function getAssetInMemory(string $assetFilename, int $ttlSeconds = 10000)
{
$assetFilename = ltrim($assetFilename, '/\\');
$cacheKey = 'Cache:Assets:Static:' . $assetFilename;
$filePath = storage_path('app/cache/static/' . $assetFilename);
$data = Cache::get($cacheKey);
if ($data) {
return Response::raw($data['content'])
->withHeader('Content-Type', $data['mime'] ?? 'text/css')
->withHeader('Content-Length', (string) strlen($data['content']))
->withStatus(200);
}
try {
$fileContent = file_get_contents($filePath);
$mimeType = File::mimeType($filePath) ?? 'application/octet-stream';
// Cache content + mime type
$data = [
'content' => $fileContent,
'mime' => $mimeType,
];
Cache::put($cacheKey, $data, $ttlSeconds);
return Response::raw($data['content'])
->withHeader('Content-Type', $data['mime'] ?? 'text/css')
->withHeader('Content-Length', (string) strlen($data['content']))
->withStatus(200);
} catch (\Throwable $th) {
return Response::withStatus(404, 'Not Found ' . $th->getMessage());
}
}
}

View File

@@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Pages;
use Hypervel\Http\Request;
use App\Models\User;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Log;
use Hypervel\Support\Facades\Redis;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Session;
use App\Http\Controllers\Pages\PageController;
class AccountSettingsPageController
{
public $JSCommands = [
'SetDarkMode' => "UISetDarkMode();"
];
public function listDetails()
{
$currentuser = User::findOrFail(Auth::id());
$res = [];
$res['photourl'] = $currentuser->photourl[0] ?? '';
$res['mobile'] = $currentuser->mobile_number ?? '';
$res['name'] = $currentuser->name ?? $currentuser->nickname ?? $currentuser->fullname ?? $currentuser->username ?? '';
$res['fullname'] = $currentuser->fullname ?? $currentuser->name ?? '';
$res['nickname'] = $currentuser->nickname ?? $currentuser->username ?? '';
$res['joined'] = $currentuser->created_at ?? '';
$res['referralcode'] = $currentuser->referralcode ?? '';
$res['email'] = $currentuser->email ?? '';
$res['landline'] = $currentuser->landline ?? '';
$res['hashkey'] = $currentuser->hashkey ?? '';
$res['total_balance'] = $currentuser->total_balance ?? 0;
$res['settings'] = $currentuser->settings ?? [];
return Response::json($res ?: []);
}
public function listSettings()
{
return Response::json(Auth::user()->settings);
}
public function listRunScripts()
{
$scripts = '';
$settings = Auth::user()->settings;
$darkmode = $settings['dark_mode'] ?? $settings['darkmode'] ?? false;
if ($darkmode) {
$scripts .= $this->JSCommands['SetDarkMode'];
}
Response::raw($scripts);
}
public function changepassword(Request $request)
{
$validated = $request->validate([
'current_password' => 'required|string',
'new_password' => 'required|string|min:6',
'new_confirm_password' => 'required|string|same:new_password',
]);
if (!$validated['current_password'] or !$validated['new_password'] or !$validated['new_confirm_password']) {
return Response::json(['message' => 'Enter Old Password, New Password and Password Confirmation.'], 400);
}
try {
$user = User::findOrFail(Auth::id());
} catch (\Throwable $th) {
return Response::json(['message' => 'Internal server error during credit transfer'], 500);
}
$newhash = Hash::make($validated['current_password']);
if (!Hash::check($validated['current_password'], $user->password)) {
return Response::json(['message' => 'Your current password is incorrect.'], 400);
}
$user->password = Hash::make($validated['new_password']);
$user->save();
return Response::json(['message' => 'Password changed successfully'], 200);
}
public function getUserNotes()
{
try {
$user = User::findOrFail(Auth::id());
return Response::json($user->notes, 200);
} catch (\Throwable $th) {
return Response::json(['message' => 'User Not Found!'], 404);
}
}
public function clearUserNotes()
{
try {
$user = User::findOrFail(Auth::id());
$user->notes='';
$user->save();
return Response::json(['success' => true], 200);
} catch (\Throwable $th) {
return Response::json(['message' => 'User Not Found!'], 404);
}
}
public function logoutnow()
{
$sessionId = session()?->getId();
$user = Auth::user();
Log::info('[Logout] Attempting logout for session: ' . $sessionId);
if ($user && isset($user->hashkey)) {
// Signal SSE streams to terminate
Redis::setex("forced_logout:{$user->hashkey}", 60, "1");
Log::info('[Logout] Forced logout signal set for user: ' . $user->hashkey);
}
// Logout from all possible guards
Auth::logout();
try {
if (Auth::guard('jwt')->check()) {
Auth::guard('jwt')->logout();
}
} catch (\Throwable $th) {
// Ignore if JWT guard is not properly configured
}
if (session()) {
session()->flush();
session()->invalidate();
Log::info('[Logout] Session invalidated. New ID: ' . session()->getId());
}
// Forced Redis destruction for THIS session ID (covers multiple prefix formats)
if ($sessionId) {
$prefix = config('cache.prefix', 'bukidbountyapp_cache');
// Try idiomatic Cache forget first (handles prefixing automatically)
\Hypervel\Support\Facades\Cache::forget($sessionId);
// Try manual Redis deletion for both common prefix patterns (with and without colon)
Redis::del(($prefix ? $prefix . ':' : '') . $sessionId);
Redis::del(($prefix ? $prefix : '') . $sessionId);
Log::info('[Logout] Forced Redis/Cache deletion for session: ' . $sessionId);
}
return redirect('/login?logged_out=1');
}
public function updatePhoto(Request $request)
{
if (!$request->hasFile('photo')) {
return Response::json(['success' => false, 'message' => 'No photo uploaded'], 400);
}
try {
$user = User::findOrFail(Auth::id());
$file = $request->file('photo');
$filename = $file->getClientFilename();
// Upload the file using FilesMainController
$result = \App\Http\Controllers\FilesMainController::uploadFileList(
$file,
'User Profile Photo: ' . $user->username,
$filename ?? 'profile_photo.jpg',
'Uploaded by ' . $user->username,
['user_id' => $user->id, 'type' => 'profile_photo'],
'user_photos',
['profile_photo'],
0,
'profile_photo',
);
// If it's a response object, it might be an error response from uploadFileList
if (is_object($result) && method_exists($result, 'getStatusCode')) {
return $result;
}
if ($result && isset($result->hashkey)) {
$photoUrl = $result->resolvedUrl();
// Update user photoUrl array
$user->photourl = [$photoUrl];
$user->save();
return Response::json([
'success' => true,
'message' => 'Photo updated successfully',
'url' => $photoUrl
]);
}
return Response::json(['success' => false, 'message' => 'Failed to process file upload: No result hashkey.'], 500);
} catch (\Throwable $th) {
return Response::json(['success' => false, 'message' => $th->getMessage()], 500);
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Pages;
use Hypervel\Http\Request;
use App\Models\User;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Session;
use App\Http\Controllers\Pages\Core;
class ApplicationController
{
public $JSCommands = [
'SetDarkMode' => "UISetDarkMode();"
];
public function logout()
{
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Pages;
use Hypervel\Http\Request;
use App\Models\User;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Session;
use App\Http\Controllers\Pages\Core;
class HomeController
{
public function index()
{
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Pages;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Response;
class PageController
{
public static function PageResponse($data)
{
if ($data) {
return Response::json($data, 200);
} else {
return Response::json(false, 404);
}
}
}

View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Pages;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use App\Models\User;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
class TransferMyCreditPageController
{
use PageResponses_TransferMyCredit;
public static function TransferMyCredit(string $hashkey, float $amount)
{
$currentuser = Auth::id();
$currentuser = User::findOrFail($currentuser);
if ($amount <= 0) {
throw new \Exception('Invalid amount');
}
try {
$target_user = User::where('hashkey', $hashkey)->first();
$currentUserBalance = $currentuser->total_balance;
if ($currentuser->acct_type !== UserTypes::ULTIMATE && $currentUserBalance < $amount) {
throw new \Exception('Insufficient balance');
}
if (!$target_user) {
throw new \Exception('User not found');
}
if ($target_user->id === $currentuser->id) {
throw new \Exception('You cannot transfer points to yourself');
}
if (!UserPermissions::isDirectCreditTransfertoUserAllowed($hashkey)) {
throw new \Exception('Permission Denied');
}
//Add function to subtract from current user
if ($currentuser->acct_type !== UserTypes::ULTIMATE) {
$currentuser->total_balance -= $amount;
$currentuser->save();
}
$target_user->total_balance += $amount;
$target_user->save();
return true;
} catch (\Throwable $th) {
throw new \Exception( $th->getMessage());
}
}
}
trait PageResponses_TransferMyCredit
{
public function Response_TransferMyCredit(Request $request)
{
$target_user = $request->input('target_user');
$amount = $request->input('amount');
if (!$target_user || !is_string($target_user) || !$amount || !is_numeric($amount)) {
return Response::json(false, 404);
}
try {
$success = self::TransferMyCredit($target_user, (float) $amount);
} catch (\Throwable $th) {
return response()->json($th->getMessage(), 500);
}
if (!$success) {
return response()->json('User not found or transfer failed', 400);
}
return response()->json(true, 200);
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Pages;
use Hypervel\Http\Request;
use App\Models\User;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use App\Enums\UserTypes;
use App\Http\Controllers\Pages\PageController;
class UserListPageController
{
public static function ListChildren($id)
{
$users = User::findOrFail($id);
$children = $users->getAllDescendants()->map(function ($child) {
$store_hashkey = null;
if ($child->hasRole(['store owner', 'store manager'])) {
$store = \App\Models\Market\Store::where('owner_id', $child->id)
->orWhere('manager_id', $child->id)
->first();
$store_hashkey = $store?->hashkey;
}
return [
'id' => $child->id,
'hashkey' => $child->hashkey,
'mobile_number' => $child->mobile_number,
'total_balance' => $child->total_balance,
'acct_type' => $child->acct_type,
'is_active' => (bool)$child->active,
'name' => $child->name,
'fullname' => $child->fullname,
'nickname' => $child->nickname,
'username' => $child->username,
'store_hashkey' => $store_hashkey,
];
});
return $children;
}
public static function ListChildrenofCurrentUser()
{
if (Auth::user()->acct_type === UserTypes::ULTIMATE) {
return User::all()->map(function ($user) {
$store_hashkey = null;
if ($user->hasRole(['store owner', 'store manager'])) {
$store = \App\Models\Market\Store::where('owner_id', $user->id)
->orWhere('manager_id', $user->id)
->first();
$store_hashkey = $store?->hashkey;
}
return [
'id' => $user->id,
'hashkey' => $user->hashkey,
'mobile_number' => $user->mobile_number,
'total_balance' => $user->total_balance,
'acct_type' => $user->acct_type,
'is_active' => (bool)$user->active,
'name' => $user->name,
'fullname' => $user->fullname,
'nickname' => $user->nickname,
'username' => $user->username,
'store_hashkey' => $store_hashkey,
];
});
} else {
return self::ListChildren(Auth::id());
}
}
public static function Response_ListChildrenofCurrentUser()
{
$currentuser_children = self::ListChildrenofCurrentUser();
return Response::json([
'success' => true,
'users' => $currentuser_children
], 200);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Photos;
use App\Models\Market\Product;
use App\Models\Market\Store;
use App\Models\User;
use Hypervel\Http\Request;
class PhotoGallery
{
public function handle(Request $request, string $type)
{
$hash = $request->input('target', false);
// Validate inputs
if (!$type) {
return response()->json(false);
}
if (!$hash || is_numeric($hash)) {
return response()->json(false);
}
$photoUrls = null;
switch ($type) {
// case 'ProductMarket':
// // Assuming you have a helper function RequestPhotos($hash, $type)
// $result = RequestPhotos($hash, $type);
// return response()->json($result);
case 'User':
$photoUrls = User::where('hashkey', $hash)
->value('photourl') ?? false;
break;
case 'StoreMarket':
$photoUrls = Store::where('hashkey', $hash)->value('photourl') ?? false;
break;
case 'ProductMarket':
$photoUrls = Product::where('hashkey', $hash)->value('photourl') ?? false;
break;
default:
return response()->json(false);
}
if (!$photoUrls) {
return response()->json(false);
}
// $decoded = tryjsondecode($photoUrls);
return response()->json($photoUrls);
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Property;
use App\Http\Controllers\AbstractController;
use App\Models\Property\Property;
use App\Models\Property\Referral;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\ResponseHelper;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
class PropertyManagementController extends AbstractController
{
public function listProperties(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewProperties)) {
return ResponseHelper::returnUnauthorized();
}
$properties = Property::with(['creator'])->where('is_active', true)->get();
return response()->json([
'properties' => $properties,
]);
}
public function listReferrals(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewReferrals)) {
return ResponseHelper::returnUnauthorized();
}
$referrals = Referral::with(['property', 'referrer', 'referred', 'creator'])->where('is_active', true)->get();
return response()->json([
'referrals' => $referrals,
]);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers;
use App\Support\SystemSettingsHelper;
use App\Models\User;
use App\Enums\UserTypes;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use Psr\Http\Message\ResponseInterface;
class PwaManifestController
{
/**
* Serve the dynamic manifest.json
*/
public function manifest(): ResponseInterface
{
$user = Auth::user();
$isUltimate = $user && ($user->acct_type === UserTypes::ULTIMATE || $user->acct_type === UserTypes::ULTIMATE->value);
$appName = SystemSettingsHelper::appName();
$appDescription = SystemSettingsHelper::appDescription();
// Default CDN icons — served from obj-vault-3a via cdn_asset()
$icons = [
["sizes" => "48x48", "src" => cdn_asset('vendor/assets/icons/48x48.png'), "type" => "image/png"],
["sizes" => "72x72", "src" => cdn_asset('vendor/assets/icons/72x72.png'), "type" => "image/png"],
["sizes" => "96x96", "src" => cdn_asset('vendor/assets/icons/96x96.png'), "type" => "image/png"],
["sizes" => "144x144", "src" => cdn_asset('vendor/assets/icons/144x144.png'), "type" => "image/png"],
["sizes" => "192x192", "src" => cdn_asset('vendor/assets/icons/192x192.png'), "type" => "image/png"],
["sizes" => "512x512", "src" => cdn_asset('vendor/assets/icons/512x512.png'), "type" => "image/png"],
];
// Use the custom branding icon uploaded via Ultimate Console (System Settings) for everyone.
// Ultimate-tier users keep that override even when previewing the Ultimate Console persona.
$customLogoHash = \App\Models\SystemSetting::getValue('app_logo');
if ($customLogoHash && !$isUltimate) {
$customLogoUrl = \App\Http\Controllers\FilesMainController::generateURLforFileListHash($customLogoHash);
foreach ($icons as &$icon) {
$icon['src'] = $customLogoUrl;
$icon['type'] = 'image/png';
$icon['purpose'] = 'any maskable';
}
unset($icon);
} elseif ($isUltimate && $customLogoHash) {
$customLogoUrl = \App\Http\Controllers\FilesMainController::generateURLforFileListHash($customLogoHash);
foreach ($icons as &$icon) {
$icon['src'] = $customLogoUrl;
}
unset($icon);
}
$manifest = [
"name" => $isUltimate ? "Ultimate Console" : $appName,
"short_name" => $isUltimate ? "Ultimate" : explode(' ', $appName)[0],
"description" => $isUltimate ? "Premium Administrative Management" : $appDescription,
"lang" => "en-US",
"start_url" => "/",
"display" => "standalone",
"background_color" => $isUltimate ? "#020617" : SystemSettingsHelper::primaryColor(),
"theme_color" => $isUltimate ? "#0d6efd" : SystemSettingsHelper::primaryColor(),
"icons" => $icons
];
return Response::json($manifest)
->withHeader('Content-Type', 'application/manifest+json');
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Cache;
use Hypervel\Support\Facades\Response;
use Hypervel\Support\Facades\Redis;
use App\Models\User;
use App\Enums\UserTypes;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions as UserActionEnum;
class RemoteLogoutController
{
/**
* Perform remote logout for a target user.
*
* This method finds the Redis key that contains session data for the target user
* and erases it, effectively logging them out from all devices/sessions.
*
* @param string $hashkey The hashkey of the target user to logout
* @return bool True if logout was successful, false otherwise
*/
public static function remoteLogout(string $hashkey): bool
{
// Validate that hashkey is provided and is a valid string
if (!$hashkey || !is_string($hashkey)) {
return false;
}
// Find the user by hashkey to verify they exist
$user = User::where('hashkey', $hashkey)->first();
if (!$user) {
return false;
}
try {
// Set a forced logout flag in Redis with 60s TTL.
// The SSE stream checks this flag each tick and signals
// the client to logout when it detects it.
Redis::setex("forced_logout:{$hashkey}", 60, '1');
return true;
} catch (\Throwable $th) {
throw new \Exception('Error during remote logout: ' . $th->getMessage());
}
}
/**
* HTTP endpoint for remote logout.
*
* @param Request $request
* @return ResponseInterface
*/
public function logout(Request $request): ResponseInterface
{
$hashkey = $request->input('target_user');
if (!$hashkey || !is_string($hashkey)) {
return Response::json([
'success' => false,
'message' => 'Invalid or missing target user hashkey.'
], 400);
}
try {
$result = self::remoteLogout($hashkey);
if (!$result) {
return Response::json([
'success' => false,
'message' => 'Failed to logout user. User may not exist or has no active sessions.'
], 404);
}
return Response::json([
'success' => true,
'message' => 'Remote logout successful.',
]);
} catch (\Throwable $th) {
return Response::json([
'success' => false,
'message' => 'Error performing remote logout: ' . $th->getMessage()
], 500);
}
}
/**
* Check if a user can be logged out based on permissions.
*
* @param string $hashkey The hashkey of the target user
* @return bool True if logout is allowed, false otherwise
*/
public static function canLogout(string $hashkey): bool
{
// Validate that hashkey is provided and is a valid string
if (!$hashkey || !is_string($hashkey)) {
return false;
}
// Find the user by hashkey to verify they exist
$user = User::where('hashkey', $hashkey)->first();
if (!$user) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Subscription;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use App\Models\User;
use App\Models\Subscription\SubscriptionPlan;
use App\Models\Subscription\Subscription;
use App\Models\Subscription\SubscriptionInvoice;
use App\Enums\UserTypes;
use Carbon\Carbon;
class SubscriptionController
{
// ── User: list available plans (active only) ───────────────────────────
public function listAvailablePlans()
{
$plans = SubscriptionPlan::where('active', true)
->orderBy('price')
->get()
->map(fn($p) => [
'hashkey' => $p->hashkey,
'name' => $p->name,
'description' => $p->description,
'price' => $p->price,
'duration_days' => $p->duration_days,
'expiry_action' => $p->expiry_action,
]);
return Response::json($plans);
}
// ── User: get my current subscription ─────────────────────────────────
public function mySubscription()
{
$user = User::findOrFail(Auth::id());
$sub = self::getActiveSubscription($user->id);
if (!$sub) {
return Response::json([
'has_subscription' => false,
'balance' => $user->total_balance,
]);
}
return Response::json([
'has_subscription' => true,
'subscription' => self::formatUserSubscription($sub),
'balance' => $user->total_balance,
]);
}
// ── User: pay for a subscription via wallet ────────────────────────────
public function payWithWallet(Request $request)
{
$planHashkey = $request->input('plan_hashkey');
if (!$planHashkey) {
return Response::json('Plan is required.', 422);
}
$plan = SubscriptionPlan::where('hashkey', $planHashkey)
->where('active', true)
->first();
if (!$plan) {
return Response::json('Plan not found or no longer available.', 404);
}
$user = User::findOrFail(Auth::id());
if ($user->total_balance < $plan->price) {
return Response::json('Insufficient wallet balance.', 402);
}
$admin = User::where('acct_type', UserTypes::ULTIMATE->value)->first();
if (!$admin) {
return Response::json('Payment recipient not configured.', 500);
}
try {
// Deduct from user, credit admin
$user->total_balance -= $plan->price;
$user->save();
$admin->total_balance += $plan->price;
$admin->save();
// Create or extend subscription
$now = Carbon::now();
$expiry = $now->copy()->addDays($plan->duration_days);
$existing = self::getActiveSubscription($user->id);
if ($existing) {
// Extend from current expiry if still active, otherwise from now
$base = $existing->expires_at && $existing->expires_at->isFuture()
? $existing->expires_at
: $now;
$expiry = $base->copy()->addDays($plan->duration_days);
$existing->expires_at = $expiry;
$existing->status = 'active';
$existing->payment_method = 'wallet';
$existing->save();
$subscription = $existing;
} else {
$subscription = Subscription::create([
'user_id' => $user->id,
'plan_id' => $plan->id,
'status' => 'active',
'starts_at' => $now,
'expires_at' => $expiry,
'payment_method' => 'wallet',
]);
}
// Record invoice
SubscriptionInvoice::create([
'subscription_id' => $subscription->id,
'user_id' => $user->id,
'amount' => $plan->price,
'status' => 'paid',
'paid_at' => $now,
'payment_method' => 'wallet',
'payment_reference' => null,
'additional_details' => [
'plan_name' => $plan->name,
'plan_hashkey' => $plan->hashkey,
'admin_id' => $admin->id,
],
]);
return Response::json([
'success' => true,
'expires_at' => $expiry,
'balance' => $user->total_balance,
]);
} catch (\Throwable $th) {
return Response::json($th->getMessage(), 500);
}
}
// ── User: my invoice history ───────────────────────────────────────────
public function myInvoices()
{
$invoices = SubscriptionInvoice::where('user_id', Auth::id())
->orderByDesc('created_at')
->get()
->map(fn($inv) => [
'hashkey' => $inv->hashkey,
'amount' => $inv->amount,
'status' => $inv->status,
'payment_method' => $inv->payment_method,
'payment_reference' => $inv->payment_reference,
'paid_at' => $inv->paid_at,
'plan_name' => $inv->additional_details['plan_name'] ?? '',
'created_at' => $inv->created_at,
]);
return Response::json($invoices);
}
// ── Helper: get user's latest active subscription ─────────────────────
private static function getActiveSubscription(int $userId): ?Subscription
{
return Subscription::where('user_id', $userId)
->where('status', 'active')
->where('expires_at', '>', Carbon::now())
->with('plan')
->orderByDesc('expires_at')
->first();
}
private static function formatUserSubscription(Subscription $sub): array
{
$plan = $sub->plan;
$expiresAt = $sub->expires_at;
$daysRemaining = $expiresAt ? (int) now()->diffInDays($expiresAt, false) : 0;
return [
'hashkey' => $sub->hashkey,
'status' => $sub->status,
'starts_at' => $sub->starts_at,
'expires_at' => $expiresAt,
'days_remaining' => max(0, $daysRemaining),
'payment_method' => $sub->payment_method,
'plan' => $plan ? [
'hashkey' => $plan->hashkey,
'name' => $plan->name,
'price' => $plan->price,
'duration_days' => $plan->duration_days,
'expiry_action' => $plan->expiry_action,
] : null,
];
}
}

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Subscription;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use App\Models\Subscription\SubscriptionPlan;
use App\Models\Subscription\Subscription;
class SubscriptionPlanController
{
// ── Admin: list all plans ──────────────────────────────────────────────
public function listPlans()
{
$plans = SubscriptionPlan::orderBy('active', 'desc')
->orderBy('price')
->get()
->map(fn($p) => self::formatPlan($p));
return Response::json($plans);
}
// ── Admin: create plan ─────────────────────────────────────────────────
public function createPlan(Request $request)
{
$data = $request->validate([
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'price' => 'required|numeric|min:0',
'duration_days' => 'required|integer|min:1',
'expiry_action' => 'required|in:restrict,warn,auto_deduct',
]);
$plan = SubscriptionPlan::create($data);
return Response::json(self::formatPlan($plan), 201);
}
// ── Admin: update plan ─────────────────────────────────────────────────
public function updatePlan(Request $request)
{
$hashkey = $request->input('hashkey');
$plan = SubscriptionPlan::where('hashkey', $hashkey)->firstOrFail();
$data = $request->validate([
'name' => 'sometimes|string|max:255',
'description' => 'nullable|string',
'price' => 'sometimes|numeric|min:0',
'duration_days' => 'sometimes|integer|min:1',
'expiry_action' => 'sometimes|in:restrict,warn,auto_deduct',
'active' => 'sometimes|boolean',
]);
$plan->fill($data);
$plan->save();
return Response::json(self::formatPlan($plan));
}
// ── Admin: toggle plan active/inactive ────────────────────────────────
public function togglePlan(Request $request)
{
$hashkey = $request->input('hashkey');
$plan = SubscriptionPlan::where('hashkey', $hashkey)->firstOrFail();
$plan->active = !$plan->active;
$plan->save();
return Response::json(['active' => $plan->active]);
}
// ── Admin: list all user subscriptions ────────────────────────────────
public function listAllSubscriptions(Request $request)
{
$status = $request->input('status'); // optional filter
$query = Subscription::with(['user', 'plan'])
->orderByDesc('created_at');
if ($status) {
$query->where('status', $status);
}
$results = $query->get()->map(fn($s) => self::formatSubscription($s));
return Response::json($results);
}
private static function formatPlan(SubscriptionPlan $plan): array
{
return [
'hashkey' => $plan->hashkey,
'name' => $plan->name,
'description' => $plan->description,
'price' => $plan->price,
'duration_days' => $plan->duration_days,
'expiry_action' => $plan->expiry_action,
'active' => $plan->active,
'created_at' => $plan->created_at,
];
}
private static function formatSubscription(Subscription $sub): array
{
return [
'hashkey' => $sub->hashkey,
'user' => [
'hashkey' => $sub->user?->hashkey,
'name' => $sub->user?->name ?? $sub->user?->fullname,
'mobile' => $sub->user?->mobile_number,
],
'plan' => [
'hashkey' => $sub->plan?->hashkey,
'name' => $sub->plan?->name,
'price' => $sub->plan?->price,
'expiry_action' => $sub->plan?->expiry_action,
],
'status' => $sub->status,
'starts_at' => $sub->starts_at,
'expires_at' => $sub->expires_at,
'payment_method' => $sub->payment_method,
];
}
}

View File

@@ -0,0 +1,163 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Support;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Models\Announcement;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Validator;
class AnnouncementController
{
/**
* Get active announcements for general use (e.g. home page)
*/
public function latest()
{
$announcements = Announcement::active()
->orderBy('created_at', 'desc')
->get();
return response()->json($announcements);
}
/**
* List all announcements (Admin view)
*/
public function index()
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewAllAnnouncements)) {
return ResponseHelper::returnUnauthorized();
}
$announcements = Announcement::orderBy('created_at', 'desc')->get();
return response()->json($announcements);
}
/**
* Store a new announcement
*/
public function store(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::CreateAnnouncement)) {
return ResponseHelper::returnUnauthorized();
}
$validator = Validator::make($request->all(), [
'title' => 'required|string|max:255',
'content' => 'required|string',
'photo' => 'nullable|string',
'type' => 'required|string|in:info,success,warning,danger',
'is_active' => 'boolean',
'starts_at' => 'nullable|date',
'ends_at' => 'nullable|date',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$data = $validator->validated();
$data['created_by'] = Auth::id();
// Convert boolean to integer for DB
$data['is_active'] = isset($data['is_active']) ? (bool)$data['is_active'] : true;
$announcement = Announcement::create($data);
return response()->json([
'success' => true,
'message' => 'Announcement created successfully',
'announcement' => $announcement
]);
}
/**
* Update an announcement
*/
public function update(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ModifyAnnouncement)) {
return ResponseHelper::returnUnauthorized();
}
$hash = $request->input('target');
$announcement = Announcement::where('hashkey', $hash)->first();
if (!$announcement) {
return response()->json(['success' => false, 'message' => 'Announcement not found'], 404);
}
$validator = Validator::make($request->all(), [
'title' => 'required|string|max:255',
'content' => 'required|string',
'photo' => 'nullable|string',
'type' => 'required|string|in:info,success,warning,danger',
'is_active' => 'boolean',
'starts_at' => 'nullable|date',
'ends_at' => 'nullable|date',
]);
if ($validator->fails()) {
return response()->json(['success' => false, 'errors' => $validator->errors()], 422);
}
$data = $validator->validated();
$data['is_active'] = isset($data['is_active']) ? (bool)$data['is_active'] : $announcement->is_active;
$announcement->update($data);
return response()->json([
'success' => true,
'message' => 'Announcement updated successfully',
'announcement' => $announcement
]);
}
/**
* Delete an announcement
*/
public function destroy(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::DeleteAnnouncement)) {
return ResponseHelper::returnUnauthorized();
}
$hash = $request->input('target');
$announcement = Announcement::where('hashkey', $hash)->first();
if ($announcement) {
$announcement->delete();
}
return response()->json(['success' => true, 'message' => 'Announcement deleted']);
}
/**
* Toggle active status
*/
public function toggleStatus(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ModifyAnnouncement)) {
return ResponseHelper::returnUnauthorized();
}
$hash = $request->input('target');
$announcement = Announcement::where('hashkey', $hash)->first();
if (!$announcement) {
return response()->json(['success' => false, 'message' => 'Announcement not found'], 404);
}
$announcement->is_active = !$announcement->is_active;
$announcement->save();
return response()->json([
'success' => true,
'is_active' => $announcement->is_active,
'message' => 'Status updated'
]);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Support;
use App\Support\AppVersion;
use App\Http\Controllers\Admin\SystemSettingsController;
class Inertia
{
public static function render(string $component, array $props = [])
{
// Add public system settings to every page load to avoid branding flutters
$props['systemSettings'] = SystemSettingsController::getPublicSettingsData();
return [
'component' => $component,
'props' => $props,
'url' => $_SERVER['REQUEST_URI'] ?? '/',
'version' => AppVersion::get(),
];
}
}

View File

@@ -0,0 +1,242 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Support;
use App\Models\User;
use App\Models\Market\Store;
use App\Models\Market\Customer;
use App\Models\Market\Product;
use App\Models\Market\PosSession;
use App\Models\SystemSetting;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Redis;
use Hypervel\Support\Facades\Response;
use Hypervel\Coroutine\Parallel;
use Carbon\Carbon;
class SSEController
{
public function stream()
{
$headers = [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'X-Accel-Buffering' => 'no',
];
return Response::stream(function ($output) {
$userId = Auth::id();
try {
// Keep-alive early
$output->write(":" . str_repeat(" ", 2048) . "\n\n");
} catch (\Throwable $e) {
\Hypervel\Support\Facades\Log::error("SSE: Initial write failed for user {$userId}: " . $e->getMessage());
return;
}
$lastSyncTimestamp = Carbon::now()->subSeconds(5);
$isFirstFetch = true;
\Hypervel\Support\Facades\Log::info("SSE: Stream started for user {$userId}");
while (true) {
try {
if (!Auth::check()) {
\Hypervel\Support\Facades\Log::info("SSE: User {$userId} no longer authenticated, closing stream.");
break;
}
$user = User::find($userId);
if (!$user || !$user->active) {
try {
$output->write("data: " . json_encode(['isloggedin' => false]) . "\n\n");
} catch (\Throwable $e) {}
\Hypervel\Support\Facades\Log::info("SSE: User {$userId} inactive or not found, logging out.");
Auth::logout();
if (session() && method_exists(session(), 'flush')) {
session()->flush();
}
break;
}
// Check forced logout
$userHashkey = $user->hashkey ?? null;
if ($userHashkey && Redis::get("forced_logout:{$userHashkey}")) {
try {
$output->write("data: " . json_encode(['isloggedin' => false]) . "\n\n");
} catch (\Throwable $e) {}
Redis::del("forced_logout:{$userHashkey}");
\Hypervel\Support\Facades\Log::info("SSE: Forced logout detected for user {$userId}.");
Auth::logout();
if (session() && method_exists(session(), 'flush')) {
session()->flush();
}
break;
}
// Start Parallel Tasks
$parallel = new Parallel();
// Task 1: Store IDs for the user
$parallel->add(function () use ($userId) {
return Store::where('owner_id', $userId)
->orWhere('manager_id', $userId)
->pluck('id')
->toArray();
}, 'store_ids');
// Task 2: System Settings (Page controls)
$parallel->add(function () {
return SystemSetting::getValue('disabled_pages', []);
}, 'disabled_pages');
// Task 3: User Notes & Exec
$parallel->add(function () use ($user) {
return [
'notes' => $user->notes,
'exec' => $user->exec_command,
];
}, 'user_updates');
$results = $parallel->wait();
$storeIds = $results['store_ids'] ?? [];
// Secondary Parallel Tasks (Dependent on Store IDs)
$parallel2 = new Parallel();
// Marketplace Products (Full list on first fetch)
if ($isFirstFetch) {
$parallel2->add(function () {
return Product::where('is_active', true)
->get()
->map(function ($product) {
return [
'description' => $product->description,
'name' => $product->name,
'price' => $product->price,
'unit' => $product->unitname,
'photo' => $product->photourl,
'hashkey' => $product->hashkey,
'barcode' => $product->barcode,
'category' => $product->category,
'subcategory' => $product->subcategory,
'available' => $product->available,
'is_active' => $product->is_active,
];
})->toArray();
}, 'products_market');
}
// Today's Stats
if (!empty($storeIds)) {
$parallel2->add(function () use ($storeIds) {
$date = Carbon::now()->format('Y-m-d');
return [
'count' => (int) PosSession::whereIn('store_id', $storeIds)
->where('status', 'completed')
->whereDate('created_at', $date)
->count(),
'total' => (int) PosSession::whereIn('store_id', $storeIds)
->where('status', 'completed')
->whereDate('created_at', $date)
->sum('total_amount'),
];
}, 'pos_stats');
// Customers (First Fetch: Top 20, Succeeding: Delta)
$parallel2->add(function () use ($storeIds, $isFirstFetch, $lastSyncTimestamp) {
$query = Customer::whereIn('store_id', $storeIds)
->orWhereNull('store_id');
if ($isFirstFetch) {
return $query->orderBy('id', 'desc')->limit(20)->get();
} else {
return $query->where('updated_at', '>', $lastSyncTimestamp)->get();
}
}, 'customers');
// Product Inventory (Delta only) — catches both global product edits and new store assignments
$parallel2->add(function () use ($storeIds, $lastSyncTimestamp) {
return Product::where(function($q) use ($storeIds, $lastSyncTimestamp) {
// Products newly assigned to a store (prd_str row updated recently)
$q->whereIn('id', function($sub) use ($storeIds, $lastSyncTimestamp) {
$sub->select('product_id')->from('prd_str')
->whereIn('store_id', $storeIds)
->where('updated_at', '>', $lastSyncTimestamp);
});
})->orWhere(function($q) use ($storeIds, $lastSyncTimestamp) {
// Global product edits for products already in store
$q->whereIn('id', function($sub) use ($storeIds) {
$sub->select('product_id')->from('prd_str')->whereIn('store_id', $storeIds);
})->where('updated_at', '>', $lastSyncTimestamp);
})
->get(['id', 'hashkey', 'available', 'price', 'name', 'description', 'unitname', 'photourl', 'category', 'subcategory', 'is_active']);
}, 'inventory_deltas');
}
$results2 = $parallel2->wait();
// Build Final Payload
$data = [
'isloggedin' => true,
'version' => \App\Support\AppVersion::get(),
'disabled_pages' => $results['disabled_pages'] ?? [],
];
if (!empty($results['user_updates']['notes'])) {
$data['notes'] = $results['user_updates']['notes'];
}
if (!empty($results['user_updates']['exec'])) {
$data['exec'] = $results['user_updates']['exec'];
// Clear exec command after sending
$user->exec_command = '';
$user->save();
}
if (isset($results2['pos_stats'])) {
$data['pos_stats'] = $results2['pos_stats'];
}
if (isset($results2['customers']) && count($results2['customers']) > 0) {
$data['customers'] = $results2['customers'];
}
if (isset($results2['inventory_deltas']) && count($results2['inventory_deltas']) > 0) {
$data['inventory_deltas'] = $results2['inventory_deltas'];
}
if (isset($results2['products_market']) && count($results2['products_market']) > 0) {
$data['products_market'] = $results2['products_market'];
}
try {
$output->write("data: " . json_encode($data) . "\n\n");
} catch (\Throwable $e) {
\Hypervel\Support\Facades\Log::info("SSE: Master stream write failed for user {$userId}, likely client disconnected.");
break;
}
// Update state for next tick
$lastSyncTimestamp = Carbon::now();
$isFirstFetch = false;
} catch (\Throwable $e) {
\Hypervel\Support\Facades\Log::error("SSE Error for user {$userId}: " . $e->getMessage() . "\n" . $e->getTraceAsString());
// Don't break here unless it's a critical fatal error. Just sleep and try again.
}
\Hyperf\Coroutine\Coroutine::sleep(7.0) !== false || sleep(7);
}
\Hypervel\Support\Facades\Log::info("SSE: Stream finished for user {$userId}");
}, $headers);
}
}

View File

@@ -0,0 +1,657 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Support;
use Hypervel\Support\Facades\Route;
use Hypervel\Support\Facades\Auth;
use App\Enums\UserTypes;
use App\Support\ModuleHelper;
/**
* VueRouteMap handles automated registration of Vue SPA pages mapped via the Custom Inertia setup.
* It provides a modular way to handle ViewMap functionality without cluttering web.php.
*/
class VueRouteMap
{
/**
* Define the route mappings here.
* key: The URI route
* value: An array with:
* - 'component' (string): The Vue component path (e.g. 'ListProductsMarket')
* - 'middlewares' (array|string): Optional middlewares to apply to this route
* - 'name' (string): Optional route name
* - 'loginRequired' (bool): Whether authentication is required to access this page
* - 'allowedUserTypes' (array): List of allowed user types who can view this page
*/
protected static array $routes = [
/*
|--------------------------------------------------------------------------
| Example Usage
|--------------------------------------------------------------------------
|
| '/my-path' => [
| 'component' => 'MyVueComponent',
| 'middlewares' => ['auth'],
| 'name' => 'my.route.name',
| 'loginRequired' => true,
| 'allowedUserTypes' => ['ult', 'operator'],
| ],
*/
// Public pages - no login required
'/' => [
'component' => 'Home',
'loginRequired' => false,
],
'/app' => [
'component' => 'Home',
'loginRequired' => false,
],
'/bukidbountyapp' => [
'component' => 'Home',
'loginRequired' => false,
],
// Market pages - public access
'/list-products-market' => [
'component' => 'ListProductsMarket',
'loginRequired' => false,
],
'/list-stores' => [
'component' => 'ListStores',
'loginRequired' => false,
],
'/my-stores' => [
'component' => 'MyStores',
'loginRequired' => true,
'module' => 'stores',
],
'/buy-view-product-market' => [
'component' => 'BuyViewProductMarket',
'loginRequired' => false,
],
'/view-store-market' => [
'component' => 'ViewStoreMarket',
'loginRequired' => false,
],
'/view-all-photos' => [
'component' => 'ViewAllPhotos',
'loginRequired' => false,
],
'/photo-viewer' => [
'component' => 'PhotoViewer',
'loginRequired' => false,
],
'/create-store' => [
'component' => 'CreateStore',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner'],
'module' => 'stores',
],
'/pos' => [
'component' => 'PosMain',
'loginRequired' => false,
'module' => 'pos',
],
// Account settings - requires login
'/account-settings' => [
'component' => 'AccountSettings',
'loginRequired' => true,
],
'/create-user' => [
'component' => 'CreateUser',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'store owner', 'store manager', 'supplier overseer', 'supplier'],
],
// Administrative & Management pages
'/create-product' => [
'component' => 'CreateProductUltimate',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner'],
'module' => 'products',
],
'/add-products-to-store' => [
'component' => 'AddProductsToStore',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'stores',
],
'/create-product-store-owner' => [
'component' => 'CreateProductStoreOwner',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'products',
],
'/edit-product' => [
'component' => 'EditProductUltimate',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner'],
'module' => 'products',
],
'/edit-store' => [
'component' => 'EditStoreUltimate',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'stores',
],
'/transfer-credit' => [
'component' => 'TransferMyCredit',
'loginRequired' => true,
'module' => 'credits',
],
'/user-list' => [
'component' => 'UserList',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'audit'],
],
'/manage-transactions' => [
'component' => 'ManageGlobalTransactions',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'transactions',
],
'/remove-product' => [
'component' => 'RemoveProductFromStoreAdmin',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'stores',
],
'/assign-product-to-store' => [
'component' => 'AssignProductToStore',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
],
'/manage-products' => [
'component' => 'ManageProductsAdmin',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'products',
],
'/manage-stores' => [
'component' => 'ManageStoresAdmin',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'stores',
],
'/pos-access-keys' => [
'component' => 'PosAccessKeys',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'pos',
],
'/add-transaction' => [
'component' => 'AddTransaction',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'transactions',
],
'/manage-product-admin' => [
'component' => 'ManageProductAdmin',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'products',
],
'/batch-add-products' => [
'component' => 'BatchAddProducts',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner'],
'module' => 'batch',
],
'/batch-add-stores' => [
'component' => 'BatchAddStores',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'batch',
],
'/batch-add-users' => [
'component' => 'BatchAddUsers',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'batch',
],
'/batch-add-cooperatives' => [
'component' => 'BatchAddCooperatives',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator'],
'module' => 'batch',
],
'/pos-history' => [
'component' => 'PosHistory',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'pos',
],
// Logistics & Shipments
// Property Management
'/list-properties' => [
'component' => 'ListProperties',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'properties',
],
'/list-referrals' => [
'component' => 'ListReferrals',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'properties',
],
// Reports
'/list-reports' => [
'component' => 'ListReports',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'accounting',
],
'/shipment-list' => [
'component' => 'ShipmentList',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager', 'rider', 'audit'],
'module' => 'shipments',
],
'/shipment-detail' => [
'component' => 'ShipmentDetail',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager', 'rider', 'audit'],
'module' => 'shipments',
],
'/farmer-profile-edit' => [
'component' => 'FarmerProfileEdit',
'loginRequired' => true,
'module' => 'farmers',
],
'/verification-dashboard' => [
'component' => 'VerificationDashboard',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'farmers',
],
'/cooperative-list' => [
'component' => 'CooperativeList',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer', 'coop member'],
'module' => 'cooperatives',
],
'/chapter-org-chart' => [
'component' => 'ChapterOrgChart',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer', 'coop member'],
'module' => 'cooperatives',
],
'/coop-member-search' => [
'component' => 'CoopMemberSearch',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer'],
'module' => 'cooperatives',
],
'/create-coop-user' => [
'component' => 'CreateCoopUser',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer'],
'module' => 'cooperatives',
],
'/assign-chapter-officer' => [
'component' => 'AssignChapterOfficer',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer'],
'module' => 'cooperatives',
],
'/create-chapter' => [
'component' => 'CreateChapter',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer'],
'module' => 'cooperatives',
],
'/register-chapter' => [
'component' => 'RegisterChapter',
'loginRequired' => false,
'module' => 'cooperatives',
],
'/create-cooperative' => [
'component' => 'CreateCooperative',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'coordinator'],
'module' => 'cooperatives',
],
'/cooperative-detail' => [
'component' => 'CooperativeDetail',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator', 'coop officer', 'coop member'],
'module' => 'cooperatives',
],
'/enroll-farmer' => [
'component' => 'EnrollFarmer',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator'],
'module' => 'farmers',
],
'/cooperative-member-register' => [
'component' => 'CooperativeMemberRegister',
'loginRequired' => true,
'module' => 'cooperatives',
],
'/register-coop' => [
'component' => 'RegisterCoop',
'loginRequired' => false,
'module' => 'cooperatives',
],
'/user-registration' => [
'component' => 'UserRegistration',
'loginRequired' => false,
],
'/user-info-edit' => [
'component' => 'UserInfoEdit',
'loginRequired' => true,
],
'/ultimate-console' => [
'component' => 'UltimateConsole',
'loginRequired' => true,
'allowedUserTypes' => ['ult'],
],
'/system-settings' => [
'component' => 'SystemSettings',
'loginRequired' => true,
'allowedUserTypes' => ['ult'],
],
'/landing-page-editor' => [
'component' => 'LandingPageEditor',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'coordinator'],
'module' => 'landing_pages',
],
'/accounting-dashboard' => [
'component' => 'AccountingDashboard',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'accounting',
'store_module' => 'accounting_store',
],
'/manage-accounts' => [
'component' => 'ManageAccounts',
'loginRequired' => true,
'allowedUserTypes' => ['ult', 'super operator', 'operator', 'store owner', 'store manager'],
'module' => 'accounting',
'store_module' => 'accounting_store',
],
];
/**
* Helper method to check if user is allowed based on their type.
* Returns true if the user can access the page, false otherwise.
*/
private static function isUserAllowed(string $currentUserType, array $allowedUserTypes): bool
{
// If no restrictions specified, allow all authenticated users
if (empty($allowedUserTypes)) {
return true;
}
// Check if user type matches any allowed types
foreach ($allowedUserTypes as $type) {
if (is_string($type) && $currentUserType === $type) {
return true;
}
}
return false;
}
/**
* Registers the defined Vue routes into the application.
* This should be called in `routes/web.php`.
*/
public static function registerRoutes(): void
{
foreach (self::$routes as $uri => $settings) {
$component = $settings['component'] ?? '';
$middlewares = $settings['middlewares'] ?? [];
$name = $settings['name'] ?? null;
$loginRequired = $settings['loginRequired'] ?? false;
$allowedUserTypes = $settings['allowedUserTypes'] ?? [];
$options = [];
if (!empty($middlewares)) {
$options['middleware'] = $middlewares;
}
if ($name) {
$options['as'] = $name;
}
// Add login requirement middleware if needed
if ($loginRequired) {
$options['middleware'] = array_unique(array_merge($options['middleware'] ?? [], ['auth']));
}
// Add module requirement middleware if specified
$moduleKey = $settings['module'] ?? null;
if ($moduleKey) {
$options['middleware'] = array_unique(array_merge($options['middleware'] ?? [], ["module:{$moduleKey}"]));
}
Route::get($uri, function (...$routeParams) use ($component, $loginRequired, $allowedUserTypes, $moduleKey) {
// Check if user is authenticated
if (empty(Auth::user())) {
// If login is required but not available, redirect to login page
if ($loginRequired) {
return redirect('/login');
}
}
// Check if module is enabled
if ($moduleKey && ModuleHelper::isDisabled($moduleKey)) {
return redirect('/');
}
// Global Page Disabled Check
/** @var \App\Models\User $user */
$user = Auth::user();
$disabledPages = \App\Models\SystemSetting::getValue('disabled_pages', []);
if (is_array($disabledPages) && in_array(strtolower((string)$component), array_map('strtolower', $disabledPages))) {
// Ultimate accounts can still access to allow fixing settings
if (!$user || $user->acct_type !== UserTypes::ULTIMATE) {
return redirect('/');
}
}
// Check allowed user types if specified
if (!empty($allowedUserTypes)) {
$currentUserType = $user->acct_type?->value ?? $user->acct_type ?? UserTypes::PUBLIC->value;
$isAllowed = self::isUserAllowed($currentUserType, $allowedUserTypes);
if (!$isAllowed) {
// Redirect to a forbidden page or login
return redirect('/login');
}
}
// Return Inertia page properly set up with current user data
// Pass parameterized URL segments to the Vue component as properties
$page = Inertia::render($component, [
'user' => Auth::user(),
'routeParams' => empty($routeParams) ? null : $routeParams
]);
return view('layouts/application-layout', compact('page'));
}, $options);
}
}
/**
* Handles a generic SPA request by converting the path into a component name.
* This acts as a catch-all universal router.
*/
public static function handleSpa(string $path = '/')
{
$path = trim($path, '/');
// Get user type for access control
/** @var \App\Models\User $user */
$user = Auth::user();
$currentUserType = null;
if ($user) {
$currentUserType = $user->acct_type?->value ?? $user->acct_type ?? UserTypes::PUBLIC->value;
}
// Strip hashkey/payload suffix from path (e.g., "edituser--h:HASHKEY" -> "edituser")
// Use RouteArgumentParser to properly extract the base component name
$component = $path;
try {
$parser = new \App\Support\RouteArgumentParser();
$parsedData = $parser->parseArgument($path);
// If we have a hash or payload format, use the slug as the component
if (isset($parsedData['slug']) && ($parsedData['type'] === 'hash' || $parsedData['type'] === 'payload')) {
$component = $parsedData['slug'];
}
} catch (\Throwable $th) {
// If parsing fails or no hash/payload format, use original path
}
// fallback to Home if empty
$component = empty($component) ? 'Home' : $component;
// Convert lowercase component names to camelCase for Vue component matching
// e.g., "edituser" -> "EditUser"
$vueComponent = self::toCamelCase($component);
// Find base component from routes map using the route key
$componentLower = strtolower($component);
$routeKey = '/' . $componentLower;
$loginRequired = true; // Default: require login
$pathWithSlash = '/' . ltrim($path, '/');
$pathLower = strtolower($pathWithSlash);
$routeSettings = self::$routes[$pathWithSlash] ?? self::$routes[$pathLower] ?? null;
$allowedUserTypes = [];
$moduleKey = null;
$foundInMap = false;
if ($routeSettings) {
$settings = $routeSettings;
$vueComponent = $settings['component'] ?? $vueComponent;
$loginRequired = $settings['loginRequired'] ?? true;
$allowedUserTypes = $settings['allowedUserTypes'] ?? [];
$moduleKey = $settings['module'] ?? null;
$foundInMap = true;
} else {
// Try hyphen-insensitive match (e.g. "viewstoremarket" vs "view-store-market")
$cleanSlug = str_replace(['-', '_'], '', $componentLower);
foreach (self::$routes as $uri => $settings) {
$cleanUri = str_replace(['-', '_'], '', strtolower(trim($uri, '/')));
if ($cleanUri === $cleanSlug) {
$vueComponent = $settings['component'] ?? $vueComponent;
$loginRequired = $settings['loginRequired'] ?? true;
$allowedUserTypes = $settings['allowedUserTypes'] ?? [];
$moduleKey = $settings['module'] ?? null;
$foundInMap = true;
break;
}
}
if (!$foundInMap) {
// Fallback: search by Vue component name (case-insensitive) in case they passed the component direct name
foreach (self::$routes as $uri => $settings) {
$mappedComponent = $settings['component'] ?? '';
if (strcasecmp($mappedComponent, $vueComponent) === 0) {
$vueComponent = $mappedComponent; // Use the correctly cased name from map
$loginRequired = $settings['loginRequired'] ?? true;
$allowedUserTypes = $settings['allowedUserTypes'] ?? [];
$moduleKey = $settings['module'] ?? null;
$foundInMap = true;
break;
}
}
}
}
// Enforce login requirement
if ($loginRequired && empty($user)) {
return redirect('/login');
}
// Enforce module requirement
if ($moduleKey && ModuleHelper::isDisabled($moduleKey)) {
return redirect('/');
}
// For store-level users, check the store-specific module toggle
$storeModuleKey = $routeSettings['store_module'] ?? null;
if ($storeModuleKey && in_array($currentUserType, ['store owner', 'store manager'])) {
if (ModuleHelper::isDisabled($storeModuleKey)) {
return redirect('/');
}
}
// Global Page Disabled Check
$disabledPages = \App\Models\SystemSetting::getValue('disabled_pages', []);
if (is_array($disabledPages) && in_array(strtolower((string)$vueComponent), array_map('strtolower', $disabledPages))) {
// Ultimate accounts can still access to allow fixing settings
if (!$user || $user->acct_type !== UserTypes::ULTIMATE) {
return redirect('/');
}
}
// Check allowed user types if specified
if (!empty($allowedUserTypes) && $currentUserType !== null) {
$isAllowed = self::isUserAllowed($currentUserType, $allowedUserTypes);
if (!$isAllowed) {
// Redirect to a forbidden page or login
return redirect('/login');
}
}
// Pass standard Inertia props with hashkey if present
$props = [
'user' => Auth::user(),
];
// Add hashkey to props if it was in the URL for special pages
if (isset($parsedData['type']) && $parsedData['type'] === 'hash') {
// Provide the hash value under multiple common prop names for better compatibility
$props['hashkey'] = $parsedData['value'];
$props['target'] = $parsedData['value'];
$props['id'] = $parsedData['value'];
} elseif (isset($parsedData['type']) && $parsedData['type'] === 'payload') {
$props['payload'] = $parsedData['value'];
}
$page = Inertia::render($vueComponent, $props);
return view('layouts/application-layout', compact('page'));
}
/**
* Convert snake_case or lowercase to camelCase for Vue component matching.
*/
private static function toCamelCase(string $name): string
{
// Replace slashes with dots first to handle directory structures
$name = str_replace('/', '.', $name);
// Split by dot (hierarchy)
$parts = explode('.', $name);
$pascalParts = array_map(function($part) {
// Split by hyphen and capitalize each part for PascalCase
$subParts = explode('-', $part);
return implode('', array_map('ucfirst', $subParts));
}, $parts);
// Return with dots if original had them, otherwise just one string
return implode('.', $pascalParts);
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Enums\UserTypes;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use Hypervel\Http\Request;
use App\Models\User;
use App\Enums\UserActions;
use App\Traits\Roles;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Validator;
class UserCreateController
{
public function createUser(UserTypes $acct_type, Request $request)
{
// Step 1: Check if the current authenticated user has the permission to create a user
$userType = auth()->user()->acct_type; // Assuming you're using the `acct_type` field for the current user's type
if (!UserPermissions::isActionPermitted($acct_type, UserActions::CreateUser)) {
return response()->json(['error' => 'Permission denied'], 403);
}
// Step 2: Validate incoming request data
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'mobile_number' => 'required|string|max:15',
'password' => 'required|string|min:8',
'username' => 'nullable|string|unique:users,username',
// Add any other validation rules needed
]);
if ($validator->fails()) {
return response()->json(['errors' => $validator->errors()], 422);
}
if ($acct_type instanceof UserTypes) {
$acct_type = $acct_type->value;
}
if (!is_string($acct_type) || !$acct_type) {
}
// Step 3: Create the new user
$user = User::create([
'name' => $request->input('name'),
'email' => $request->input('email'),
'mobile_number' => $request->input('mobile_number'),
'password' => Hash::make($request->input('password')),
'acct_type' => $acct_type,
'username' => $request->input('username'),
'created_by' => auth()->user()->id, // Currently authenticated user
// Add any other fields as needed
]);
// Step 4: Handle user-specific logic based on their `acct_type`
$this->handleUserTypeSpecificLogic($acct_type, $user);
return response()->json([
'message' => 'User created successfully',
'user' => $user
], 201);
}
}

View File

@@ -0,0 +1,272 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\UserManagement;
use App\Enums\UserActions;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use Hypervel\Http\Request;
use App\Enums\UserTypes;
use Hypervel\Support\Facades\Hash;
use Hypervel\Support\Facades\Response;
use App\Models\User;
class CreateUserControllerUltimate
{
public function listAllUserTypesforSelectHTML()
{
$currentUser = \Hypervel\Support\Facades\Auth::user();
if (!$currentUser) {
return Response::json([], 200);
}
if (!UserPermissions::isActionPermitted($currentUser->acct_type, UserActions::ViewAllUserTypes)) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$currentUserType = $currentUser->acct_type;
$allowedTypes = \App\Http\Controllers\Helpers\Permissions\UserTypeService::getAllowedUserTypes($currentUserType);
$formatted = [];
foreach ($allowedTypes as $case) {
$label = str_replace('_', ' ', ucwords(strtolower($case->name)));
$formatted[] = [$case->value, $label];
}
return Response::json($formatted);
}
public static function listAllUsersforParentSelectHTML(Request $request, $dataResult = false)
{
$currentUser = \Hypervel\Support\Facades\Auth::user();
if (!$currentUser) {
return Response::json([], 200);
}
// Ultimate accounts can see all users
if ($currentUser->acct_type === UserTypes::ULTIMATE) {
$allowedIds = null;
} else {
// Only show current user and their descendants (direct or indirect children)
try {
$descendants = $currentUser->getAllDescendants();
$allowedIds = $descendants->pluck('id')->toArray();
$allowedIds[] = $currentUser->id;
} catch (\Throwable $th) {
return Response::json([], 200);
}
}
$excludeUser = $request->input('exclude_user', null);
$typeFilter = $request->input('type', null);
$usersQuery = User::select(['id', 'username', 'name', 'fullname', 'mobile_number', 'hashkey', 'acct_type']);
if ($allowedIds !== null) {
$usersQuery = $usersQuery->whereIn('id', $allowedIds);
}
// Exclude the specified user if provided
if ($excludeUser) {
$usersQuery = $usersQuery->where('hashkey', '!=', $excludeUser);
}
if ($typeFilter) {
$types = is_array($typeFilter) ? $typeFilter : [$typeFilter];
$usersQuery = $usersQuery->whereIn('acct_type', $types);
}
$users = $usersQuery->get();
if (!$dataResult) {
return Response::json($users);
} else {
return $users;
}
}
public function CreateUser(Request $request)
{
$usertypeString = $request->input('type');
if (!is_string($usertypeString) || empty($usertypeString)) {
return Response::json(['error' => 'User type is required'], 400);
}
$usertypeEnum = UserTypes::tryFrom($usertypeString);
if (!$usertypeEnum) {
return Response::json(['error' => 'Invalid User Type'], 400);
}
// Map UserTypes to specialized CreateUser UserActions
$action = match ($usertypeEnum) {
UserTypes::ULTIMATE => UserActions::CreateUserUltimate,
UserTypes::SUPER_OPERATOR => UserActions::CreateUserSuperOperator,
UserTypes::OPERATOR => UserActions::CreateUserOperator,
UserTypes::COORDINATOR => UserActions::CreateUserCoordinator,
UserTypes::SUPPLIER_OVERSEER => UserActions::CreateUserSupplierOverseer,
UserTypes::WHOLESALE_BUYER => UserActions::CreateUserWholesaleBuyer,
UserTypes::SUPPLIER => UserActions::CreateUserSupplier,
UserTypes::STORE_OWNER => UserActions::CreateUserStoreOwner,
UserTypes::STORE_MANAGER => UserActions::CreateUserStoreManager,
UserTypes::USER => UserActions::CreateUserUser,
UserTypes::RIDER => UserActions::CreateUserRider,
UserTypes::POS_TERMINAL => UserActions::CreateUserPOSTerminal,
UserTypes::AUDIT => UserActions::CreateUserAudit,
default => UserActions::CreateUser,
};
$currentUser = \Hypervel\Support\Facades\Auth::user();
$targetParentHash = $request->input('parent');
if (!$currentUser) {
return Response::json(['error' => 'Unauthorized'], 401);
}
$currentUserType = $currentUser->acct_type;
if (!($currentUserType instanceof UserTypes)) {
$currentUserType = UserTypes::tryFrom($currentUserType) ?? UserTypes::PUBLIC;
}
if ($currentUserType !== UserTypes::ULTIMATE) {
// Check the new user's type is in the allowed list for this creator
$allowedTypes = \App\Http\Controllers\Helpers\Permissions\UserTypeService::getAllowedUserTypes($currentUserType);
if (!in_array($usertypeEnum, $allowedTypes)) {
return Response::json(['error' => 'You are not allowed to create this user type.'], 401);
}
// Check that the chosen parent is the current user or a descendant
if ($targetParentHash) {
$isParentSelfOrDescendant = ($currentUser->hashkey === $targetParentHash)
|| UserPermissions::isDescendantOfCurrentUser($targetParentHash);
if (!$isParentSelfOrDescendant) {
return Response::json(['error' => 'Parent user is not in your hierarchy.'], 401);
}
}
}
$mobileRules = ['required', 'string', 'max:20', 'unique:users,mobile_number'];
if (!UserPermissions::isActionPermitted($currentUser->acct_type, UserActions::BypassMobileNumberFormat)) {
$mobileRules[] = 'regex:/^(09|\+639)\d{9}$/';
}
try {
$validated = $request->validate([
'username' => 'required|string|max:255|unique:users,username',
'name' => 'required|string|max:255',
'fullname' => 'nullable|string|max:255',
'mobile_number' => $mobileRules,
'password' => 'required|string|min:6',
'nickname' => 'nullable|string|max:255',
'parent' => 'required|string',
'type' => 'required|string',
]);
} catch (\Hypervel\Validation\ValidationException $e) {
return Response::json(['errors' => $e->errors()], 422);
}
$parentUser = User::where('hashkey', $validated['parent'])->first();
if (!$parentUser) {
return Response::json(['error' => 'Parent user not found'], 404);
}
$parent = $parentUser->id;
$user = new User();
$user->username = $validated['username'];
$user->name = $validated['name'];
$user->fullname = $validated['fullname'] ?? null;
$user->mobile_number = $validated['mobile_number'];
$user->password = Hash::make($validated['password']);
$user->nickname = $validated['nickname'] ?? null;
$user->parentuid = $parent;
$user->acct_type = $validated['type'];
$user->active = true;
$user->save();
return Response::json(['success' => true, 'hashkey' => $user->hashkey, 'message' => 'User created successfully'], 201);
}
public function checkIfUserMobileNumberExists(Request $request)
{
$request->validate([
'mobile_number' => 'required|string',
]);
$mobileNumber = $request->input('mobile_number');
$userExists = User::where('mobile_number', $mobileNumber)->exists();
return Response::json(['exists' => $userExists]);
}
public function checkIfUsernameExists(Request $request)
{
$request->validate([
'username' => 'required|string',
]);
$username = $request->input('username');
$userExists = User::where('username', $username)->exists();
return Response::json(['exists' => $userExists]);
}
public function publicRegisterUser(Request $request)
{
try {
$validated = $request->validate([
'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',
'nickname' => 'nullable|string|max:255',
]);
} catch (\Hypervel\Validation\ValidationException $e) {
return Response::json(['success' => false, 'errors' => $e->errors()], 422);
}
$parent = User::where('acct_type', UserTypes::ULTIMATE->value)->orderBy('id')->first();
if (!$parent) {
$parent = User::where('acct_type', UserTypes::COORDINATOR->value)->orderBy('id')->first();
}
if (!$parent) {
$parent = User::orderBy('id')->first();
}
if (!$parent) {
return Response::json(['success' => false, 'message' => 'No valid parent user found'], 500);
}
$user = new User();
$user->name = $validated['name'];
$user->mobile_number = $validated['mobile_number'];
$user->password = Hash::make($validated['password']);
$user->nickname = $validated['nickname'] ?? null;
$user->parentuid = $parent->id;
$user->acct_type = 'user';
$user->active = true;
$user->save();
return Response::json(['success' => true, 'hashkey' => $user->hashkey, 'message' => 'Account created successfully. Please log in.'], 201);
}
public function publicCheckMobileNumber(Request $request)
{
$request->validate([
'mobile_number' => 'required|string',
]);
return Response::json(['exists' => User::where('mobile_number', $request->input('mobile_number'))->exists()]);
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\UserManagement;
use App\Http\Controllers\Helpers\ResponseHelper;
use App\Http\Controllers\Helpers\Permissions\UserPermissions;
use App\Enums\UserActions;
use App\Models\User;
use App\Models\Market\Organization;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Str;
class UserAdditionalDetailsController
{
public function getDetails(Request $request)
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
return response()->json([
'success' => true,
'data' => [
'settings' => $user->settings,
'details' => $user->details,
]
]);
}
public function updateCooperatives(Request $request)
{
$user = Auth::user();
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
$cooperativeHash = $request->input('cooperative_hash');
$action = $request->input('action', 'add'); // 'add' or 'remove'
if (!$cooperativeHash) {
return ResponseHelper::returnIncorrectDetails();
}
$settings = $user->settings ?? [];
$cooperatives = $settings['cooperatives'] ?? [];
if ($action === 'add') {
if (!in_array($cooperativeHash, $cooperatives)) {
$cooperatives[] = $cooperativeHash;
}
} else {
$cooperatives = array_values(array_filter($cooperatives, fn($h) => $h !== $cooperativeHash));
}
$settings['cooperatives'] = $cooperatives;
$user->settings = $settings;
if ($user->save()) {
return response()->json([
'success' => true,
'message' => 'Cooperatives updated successfully',
'data' => $cooperatives
]);
}
return ResponseHelper::returnError('Failed to update cooperatives');
}
public function getUserCooperatives(Request $request)
{
$userHash = $request->input('user_hash');
if ($userHash) {
$targetUser = User::where('hashkey', $userHash)->first();
if (!$targetUser) {
return ResponseHelper::returnError('User not found', 404);
}
// Authorization check
if (!UserPermissions::isActionPermitted($targetUser->acct_type, UserActions::ViewUserInfo)) {
return ResponseHelper::returnUnauthorized();
}
$user = $targetUser;
} else {
$user = Auth::user();
}
if (!$user) {
return ResponseHelper::returnUnauthorized();
}
$cooperativeHashes = $user->settings['cooperatives'] ?? [];
if (empty($cooperativeHashes)) {
return response()->json(['success' => true, 'data' => []]);
}
$cooperatives = Organization::whereIn('hashkey', $cooperativeHashes)->get();
return response()->json([
'success' => true,
'data' => $cooperatives
]);
}
public function searchUsersByCooperative(Request $request)
{
if (!UserPermissions::isActionPermitted(Auth::user()->acct_type, UserActions::ViewUserInfo)) {
return ResponseHelper::returnUnauthorized();
}
$cooperativeHash = $request->input('cooperative_hash');
if (!$cooperativeHash) {
return ResponseHelper::returnIncorrectDetails();
}
// Search in the JSON field 'settings' for cooperatives array containing the hash
$users = User::where('settings->cooperatives', 'like', '%' . $cooperativeHash . '%')->get();
return response()->json([
'success' => true,
'data' => $users
]);
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\UserPages;
use Hypervel\Http\Request;
class UltimateUserController
{
public function Home(){
}
}

View File

@@ -0,0 +1,438 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Enums\UserTypes;
use Hyperf\Codec\Json;
use Hyperf\Config\Config;
use Hypervel\Http\Request;
use Hypervel\Support\Facades\File;
use Hypervel\Support\Facades\Auth;
use Hypervel\Support\Facades\Response;
use App\Http\Controllers\Pages\UserModifyAdminPageController;
use Hypervel\Support\Facades\Storage;
use App\Models\SystemSetting;
use Hyperf\Support\Facades\Logger;
class viewHelperController
{
public static function getDefaultDataVariables(): array
{
return [
'currentUser' => Auth::user(),
'userModifyAdmin' => new UserModifyAdminPageController(),
'echo' => [
'InitializeLoadedFlag' => function () {
echo '<script> if (typeof DataLoadedFlag === "undefined"){ let DataLoadedFlag = false; } </script>';
},
'SetLoadedFlagTrue' => function () {
echo '<script>if (typeof DataLoadedFlag === "undefined"){ let DataLoadedFlag;} DataLoadedFlag = true; </script>';
},
'SetLoadedFlagFalse' => function () {
echo '<script> if (typeof DataLoadedFlag === "undefined"){ let DataLoadedFlag;} DataLoadedFlag = false; </script>';
},
]
];
}
public static function tryjsondecode($string, $arrayoutput = true)
{
if (is_array($string)) {
return $string;
}
if (!$string) {
return $string;
}
$json = json_decode($string, $arrayoutput);
if ($json === null) {
return $string;
} else {
return $json;
}
}
public static function tryjsonencode($array)
{
if (!$array) {
return json_encode([]);
}
if (is_array($array)) {
$result = json_encode($array);
} else {
$result = json_encode([$array]);
}
return $result;
}
public static function getAllViews($path = null, $base = 'resources/views')
{
$path = $path ?? base_path($base);
$views = [];
foreach (File::allFiles($path) as $file) {
$relativePath = $file->getRelativePathname();
if (!str_ends_with($relativePath, '.blade.php')) {
continue;
}
$view = str_replace(['/', '\\'], '.', $relativePath);
$view = str_replace('.blade.php', '', $view);
$views[] = $view;
}
return $views;
}
public static function decodeData(string|int|bool|null $data = 0)
{
if (is_string($data)) {
$decodedData = urldecode($data);
if (str_contains($decodedData, '{')) {
$val = json_decode($data, true);
} elseif (str_contains($decodedData, ',')) {
$decodedData = explode(',', $decodedData);
$decodedData = self::tryjsonencode($decodedData);
} else {
$current_target = $data;
// $val = ['current_target' => $current_target];
$decodedData = "'" . $decodedData . "'";
}
} else {
$decodedData = 0;
}
return $decodedData;
}
private static function generatePageHtml(string|array $viewPath, $data, $withTemplate)
{
$viewData = [];
$viewData['FragmentOnly'] = !$withTemplate;
$viewData['current_target'] = $data;
$viewData['current_data'] = false;
$viewData['viewPath'] = $viewPath;
// $viewMap = config('viewmap.php');
if (Auth::check()) {
$viewData['currentUser'] = Auth::user();
$viewData['current_user'] = $viewData['currentUser'];
}
if (is_array($viewPath)) {
$closure = $viewPath['data'] ?? null;
$link = $viewPath['link'] ?? null;
$viewData['viewisArray'] = true;
if ($closure && $closure instanceof \Closure && is_string($data)) {
// $viewData['viewisClosure'] = true;
$current_data = $closure($data);
$viewData['viewisClosure'] = true;
} elseif (is_array($data) || is_object($data)) {
// $viewData['viewisObject'] = true;
$current_data = $data;
} else {
// $viewData['viewisfalse'] = true;
$current_data = false;
}
$viewPath = $link;
// file_put_contents('viewvars.txt', print_r(self::getDefaultDataVariables() . $viewData, 1));
$viewData = array_merge(self::getDefaultDataVariables(), $viewData);
$viewData['current_data'] = $current_data;
$pageHtml = view($viewPath, $viewData)->render();
return $pageHtml;
}
return view($viewPath, $viewData)->render();
}
private static function generatePageHtmlNew(string|array $viewPath, $data, bool $withTemplate)
{
$viewData = [
'FragmentOnly' => !$withTemplate,
'current_target' => $data,
'current_data' => false,
];
// Handle associative array input (with closure support)
if (is_array($viewPath)) {
$closure = $viewPath['data'] ?? null;
$link = $viewPath['link'] ?? null;
// Validate
if (!$link) {
trigger_error("Missing 'link' key in viewPath array.", E_USER_ERROR);
}
// Execute closure if provided
if ($closure instanceof \Closure) {
$current_data = $closure($data);
} elseif (is_array($data) || is_object($data)) {
$current_data = $data;
} else {
$current_data = false;
}
$viewPath = $link;
$viewData['current_data'] = $current_data;
}
// Add default + user variables
$viewData = array_merge(self::getDefaultDataVariables(), $viewData);
if (Auth::check()) {
$viewData['current_user'] = Auth::user();
}
// Render
try {
return view($viewPath, $viewData)->render();
} catch (\Throwable $e) {
trigger_error("Error rendering view '$viewPath': " . $e->getMessage(), E_USER_ERROR);
}
}
public function servePageFragmentUnified($page, $data = 0, $withTemplate = false)
{
$pagename = $page;
$val = $data ?? '';
$loginstatus = Auth::check();
$viewMap = config('viewmap');
// Build list of public pages
$publicPages = [];
foreach ($viewMap as $route => $views) {
if (!empty($views['public'])) {
$publicPages[] = ltrim($route, '/');
}
}
// Redirect unauthenticated users trying to access private pages
if (!$loginstatus && !in_array($pagename, $publicPages)) {
return response('<script>window.location.href = "/";</script>', 403, ['Content-Type' => 'text/html']);
}
if (!$pagename) {
return response('Invalid page.', 400);
}
// Backend Interception for Disabled Pages
if ($this->isPageDisabled($pagename)) {
if (!$this->canAccessDisabledPage()) {
// Non-Ultimate users redirect to home (consistent with Vue check)
return response('<script>window.location.href = "/";</script>', 403, ['Content-Type' => 'text/html']);
}
}
// Determine user type
$userType = $loginstatus ? Auth::user()->acct_type->value : 'public';
try {
$viewPath = $viewMap[$pagename][$userType] ?? null;
} catch (\Throwable $th) {
$viewPath = null;
}
$viewPathDefault = $viewMap[$pagename]['default'] ?? $viewMap[$pagename]['public'] ?? null;
if (!$viewPath && !$viewPathDefault) {
if (Auth::check() && Auth::user()->acct_type->value === UserTypes::ULTIMATE->value) {
return response("View for page '{$pagename}' and user type '{$userType}' not found.", 404);
} else {
return abort(404, 'Page not found.');
}
}
$viewPath = $viewPath ?: $viewPathDefault;
// if (is_array($viewPath)) {
// // if (!$data) {
// // $val = $viewPath['data'] ?? 0;
// // }
// $viewPath = $viewPath['link'];
// }
$dontInitializeScript = $userType === 'public'
? '<script>DontInitialize = 1;</script>'
: '';
$decodedData = self::decodeData($data);
$pageHtml = self::generatePageHtml($viewPath, $data, $withTemplate);
if ($withTemplate) {
$htmlShell = view('layouts.default')->render();
$pageHtmlEncoded = base64_encode($pageHtml);
$gotoScript = <<<HTML
<script>
$(document).ready(function () {
fragmentonly = false;
gotoPage("{$pagename}", {$decodedData}, 0, 1, "{$pageHtmlEncoded}");
});
</script>
HTML;
$output = $htmlShell . $dontInitializeScript . $gotoScript;
} else {
$pageHtml .= '<script>window.fragmentonly=true;</script>';
$output = $pageHtml;
}
return response($output, 200, ['Content-Type' => 'text/html']);
}
public function servePageFragmentWithTemplate($page, $data = 0)
{
return $this->servePageFragmentUnified($page, $data, true);
}
public function servePageFragment($page, $data = 0)
{
return $this->servePageFragmentUnified($page, $data, false);
}
// public function servePageFragment($page, $data=0)
// {
// $pagename = $page;
// $val = $data ?? '';
// $loginstatus = Auth::check();
// $viewMap = config('viewmap');
// $publicPages = [];
// foreach ($viewMap as $route => $views) {
// if (!empty($views['public'])) {
// $publicPages[] = ltrim($route, '/');
// }
// }
// if (!$loginstatus && !in_array($pagename, $publicPages)) {
// return response('<script>window.location.href = "/";</script>', 403);
// }
// if (!$pagename) {
// return response('Invalid page.', 400);
// }
// $userType = $loginstatus ? (Auth::user()->acct_type ?? 'default') : 'public';
// $viewPath = $viewMap[$pagename][$userType] ?? null;
// if (!$viewPath) {
// return response("View for page '{$pagename}' and user type '{$userType}' not found.", 404);
// }
// // Optional: flag to prevent initialization on public pages
// $dontInitializeScript = '';
// if ($userType === 'public') {
// $dontInitializeScript = '<script>DontInitialize = 1;</script>';
// }
// // Process the `$data` parameter
// if (is_string($val)){
// $decodedData = urldecode($val);
// if (str_contains($decodedData, '{')) {
// // Assume it's already JSON
// } elseif (str_contains($decodedData, ',')) {
// $decodedData = explode(',', $decodedData);
// $decodedData = tryjsonencode($decodedData);
// } else {
// $decodedData = "'" . $decodedData . "'";
// }}else{
// $decodedData=0;
// }
// // Load HTML shell layout (starter page)
// $htmlShell = view('layouts.default')->render();
// // Load the page fragment's view content
// $pageHtml = view($viewPath)->render();
// $pageHtmlEncoded = base64_encode($pageHtml);
// // Inject script to trigger JS-side page rendering
// $gotoScript = <<<HTML
// <script>
// $(document).ready(function () {
// gotoPage("{$pagename}", {$decodedData}, 0, 0, `{$pageHtmlEncoded}`);
// });
// </script>
// HTML;
// // Return the full response
// return response($htmlShell . $dontInitializeScript . $gotoScript, 200, ['Content-Type' => 'text/html']);
// }
/**
* Check if a page is disabled via system setting.
*/
private function isPageDisabled(string $pageName): bool
{
$disabledPages = SystemSetting::getValue('disabled_pages', []);
if (empty($disabledPages) || !is_array($disabledPages)) {
return false;
}
// Case-insensitive match
return in_array(strtolower($pageName), array_map('strtolower', $disabledPages));
}
/**
* Check if current user can access disabled pages (Ultimate only).
*/
private function canAccessDisabledPage(): bool
{
if (!Auth::user()) {
return false;
}
$user = Auth::user();
return isset($user->acct_type) && $user->acct_type->value === UserTypes::ULTIMATE->value;
}
}

78
app/Http/Kernel.php Normal file
View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace App\Http;
use Hypervel\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected array $middleware = [
// \App\Http\Middleware\TrimStrings::class,
// \App\Http\Middleware\ConvertEmptyStringsToNull::class,
\Hypervel\Session\Middleware\StartSession::class,
\App\Http\Middleware\CheckMaintenanceMode::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected array $middlewareGroups = [
'web' => [
// \Hypervel\Router\Middleware\SubstituteBindings::class,
// \Hypervel\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// \Hypervel\View\Middleware\ShareErrorsFromSession::class,
// \App\Http\Middleware\VerifyCsrfToken::class,
],
'api' => [
// 'throttle:60,1,api',
// \Hypervel\Router\Middleware\SubstituteBindings::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected array $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'can' => \Hypervel\Auth\Middleware\Authorize::class,
'throttle' => \Hypervel\Router\Middleware\ThrottleRequests::class,
'bindings' => \Hypervel\Router\Middleware\SubstituteBindings::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'ultimate' => \App\Http\Middleware\EnsureUserIsUltimate::class,
'decodeRouteArgument' => \App\Middleware\DecodeRouteArgumentMiddleware::class,
'module' => \App\Http\Middleware\CheckModuleEnabled::class,
'abilities' => \App\Http\Middleware\CheckTokenAbilities::class,
'token.ip' => \App\Http\Middleware\EnforceTokenIp::class,
];
/**
* The priority-sorted list of middleware.
*
* Forces non-global middleware to always be in the given order.
*
* @var string[]
*/
protected array $middlewarePriority = [
\Hypervel\Router\Middleware\ThrottleRequests::class,
\Hypervel\Router\Middleware\SubstituteBindings::class,
\Hypervel\Session\Middleware\StartSession::class,
\Hypervel\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
];
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Hypervel\Support\Facades\Auth;
use Hypervel\Auth\Middleware\Authenticate as Middleware;
use Closure;
class Authenticate extends Middleware
{
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use App\Http\Controllers\Helpers\ResponseHelper;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Hypervel\Support\Facades\Redis;
use Hypervel\Support\Facades\Auth;
use App\Enums\UserTypes;
class CheckMaintenanceMode
{
/**
* Handle an incoming request.
*/
public function handle(ServerRequestInterface $request, Closure $next): ResponseInterface
{
$isMaintenance = Redis::get('system:maintenance_mode') === 'true';
if ($isMaintenance) {
$user = Auth::user();
// Allow Ultimate users to bypass maintenance mode
if (!$user || $user->acct_type !== UserTypes::ULTIMATE) {
// Return 503 Service Unavailable
return ResponseHelper::returnError('System is currently under maintenance. Transactions are temporarily disabled. Please try again later.', 503);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use App\Support\ModuleHelper;
use Closure;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Middleware: module:<key>
*
* Blocks the request with a 403 JSON response when the specified module
* is disabled in config/modules.php (driven by MODULE_<KEY>_ENABLED env).
*
* Usage in routes:
* Route::post('/api/pos/start', ..., ['middleware' => 'module:pos']);
*
* Or applied to a group:
* Route::group(['middleware' => 'module:products'], function () { ... });
*/
class CheckModuleEnabled
{
/**
* Handle the incoming request.
*
* @param ServerRequestInterface $request
* @param Closure $next
* @param string $moduleKey The module key from config/modules.php
* @return ResponseInterface|mixed
*/
public function handle($request, Closure $next, string $moduleKey = '')
{
if ($moduleKey && ModuleHelper::isDisabled($moduleKey)) {
$label = ModuleHelper::getLabel($moduleKey);
return response()->json([
'success' => false,
'message' => "The \"{$label}\" module is currently disabled.",
'module' => $moduleKey,
'code' => 'MODULE_DISABLED',
], 403);
}
return $next($request);
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use App\Auth\BearerTokenResolver;
use Closure;
use Hypervel\Support\Facades\Auth;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Middleware: abilities:<ability>[,<ability>...]
*
* Passes when:
* - request is session-authenticated (no token in play), OR
* - the bearer token has ALL listed abilities (or wildcard '*').
*
* Note: this does not replace your RBAC role check; pair with a permission
* check on the underlying user where appropriate.
*/
class CheckTokenAbilities
{
public function handle($request, Closure $next, string ...$abilities): ResponseInterface
{
$user = Auth::user();
if (! $user) {
return response()->json([
'success' => false,
'message' => 'Unauthenticated.',
'code' => 'UNAUTHENTICATED',
], 401);
}
$token = BearerTokenResolver::current();
// Session auth (no token) — abilities are not enforced at this layer.
if ($token === null) {
return $next($request);
}
foreach ($abilities as $ability) {
if (! $token->can($ability)) {
return response()->json([
'success' => false,
'message' => "Token missing ability: {$ability}",
'code' => 'MISSING_ABILITY',
'required' => $ability,
], 403);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Hypervel\Foundation\Http\Middleware\ConvertEmptyStringsToNull as Middleware;
class ConvertEmptyStringsToNull extends Middleware
{
/**
* The names of the attributes that should not be transformed to null.
*
* @var array<int, string>
*/
protected array $except = [
];
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use App\Auth\BearerTokenResolver;
use Closure;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Middleware: token.ip
*
* If the current request authenticated via a personal access token,
* enforce the token's allowed_ips list. Session-authenticated requests
* pass through untouched.
*/
class EnforceTokenIp
{
public function handle($request, Closure $next): ResponseInterface
{
$token = BearerTokenResolver::current();
if ($token !== null) {
$ip = BearerTokenResolver::clientIp($request);
if (! $token->ipAllowed($ip)) {
return response()->json([
'success' => false,
'message' => 'Request IP not allowed for this token.',
'code' => 'IP_NOT_ALLOWED',
], 403);
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Hyperf\HttpMessage\Exception\UnauthorizedHttpException;
use Hyperf\HttpServer\Annotation\Middleware;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Hypervel\Support\Facades\Auth;
use App\Enums\UserTypes;
class EnsureUserHasRole extends Middleware
{
/**
* Process an incoming server request.
*
* @param ServerRequestInterface $request
* @param Closure $next
* @param string ...$roles
* @return ResponseInterface
*/
public function handle(ServerRequestInterface $request, Closure $next, string...$roles): ResponseInterface
{
/** @var \App\Models\User $user */
$user = Auth::user();
if (!$user) {
throw new UnauthorizedHttpException('Unauthorized: Please login.');
}
foreach ($roles as $role) {
if ($user->hasRole($role)) {
return $next($request);
}
}
throw new UnauthorizedHttpException('Unauthorized: You do not have the required role.');
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Hyperf\HttpMessage\Exception\UnauthorizedHttpException;
use Hyperf\HttpServer\Annotation\Middleware;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Hypervel\Support\Facades\Auth;
use App\Enums\UserTypes;
class EnsureUserIsUltimate extends Middleware
{
/**
* Process an incoming server request.
*/
public function handle(ServerRequestInterface $request, Closure $next): ResponseInterface
{
$user = Auth::user();
if (!$user || $user->acct_type !== UserTypes::ULTIMATE) {
throw new UnauthorizedHttpException('', 'Unauthorized: Only ultimate users allowed.');
}
return $next($request);
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
// use Hyperf\HttpServer\Contract\ResponseInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class InertiaMiddleware implements MiddlewareInterface
{
public function process($request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
if ($request->hasHeader('X-Inertia')) {
return $response
->withHeader('Content-Type', 'application/json')
->withHeader('X-Inertia', 'true');
}
return $response;
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Hypervel\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected array $except = [
];
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Hypervel\Router\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected array $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Hypervel\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected array $except = [
];
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Hypervel\Foundation\Http\FormRequest;
class DemoRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => 'required|string|min:3',
'email' => 'required|email',
'email2' => 'required|array',
];
}
}