initial: bootstrap from BukidBountyApp base
This commit is contained in:
774
app/Http/Controllers/Market/PosController.php
Normal file
774
app/Http/Controllers/Market/PosController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user