migrate to gtea from bistbucket

This commit is contained in:
2026-03-15 17:08:23 +07:00
commit 129ca2260c
3716 changed files with 566316 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
<?php
namespace App\Helper;
use Carbon\Carbon;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Session;
class DomainHelper
{
/**
* Get domain info from params or APP_URL, store in session.
*/
public static function getDomainInfo(?string $domain = null): array
{
$from = 'host';
if ($domain) {
$from = 'params';
} else {
$appUrl = config('app.url');
$parsedUrl = parse_url($appUrl, PHP_URL_HOST);
$domain = $parsedUrl ?: $appUrl;
}
// Check if info already in session
$sessionKey = 'domain_info_'.md5($domain);
if (Session::has($sessionKey)) {
$info = Session::get($sessionKey);
$info['from'] = $from;
return $info;
}
try {
$client = new Client(['timeout' => 10]);
$url = 'https://rdap.org/domain/'.urlencode($domain);
$res = $client->get($url);
$json = json_decode((string) $res->getBody(), true);
$registration = null;
$expiration = null;
$eventsList = [];
if (! empty($json['events']) && is_array($json['events'])) {
foreach ($json['events'] as $ev) {
if (! empty($ev['eventAction']) && ! empty($ev['eventDate'])) {
$type = strtolower($ev['eventAction']);
$date = $ev['eventDate'];
$eventsList[] = [
'type' => $type,
'date' => $date,
];
if (strpos($type, 'registration') !== false) {
$registration = $date;
}
if (strpos($type, 'expiration') !== false) {
$expiration = $date;
}
}
}
}
$info = [
'domain' => $domain,
'registration' => $registration ? Carbon::parse($registration)->format('d-F-Y h:i A') : null,
'expiration' => $expiration ? Carbon::parse($expiration)->format('d-F-Y h:i A') : null,
'events' => $eventsList,
'registrar' => $json['entities'][0]['vcardArray'][1][1][3] ?? $json['registrar'] ?? null,
'status' => $json['status'] ?? [],
'nameservers' => array_map(fn ($ns) => $ns['ldhName'] ?? null, $json['nameservers'] ?? []),
'from' => $from,
];
// Store in session
Session::put($sessionKey, $info);
return $info;
} catch (Exception $e) {
return [
'success' => false,
'message' => $e->getMessage(),
'from' => $from,
];
}
}
/**
* Check expiration warning (within 60 days, strong alert under 30 days)
*
* @param string|null $expirationDate ISO8601
* @return array ['warning' => bool, 'days_left' => int, 'level' => string, 'message' => ?string]
*/
public static function expirationWarning(?string $expirationDate): array
{
if (! $expirationDate) {
return [
'warning' => false,
'days_left' => null,
'level' => 'none',
'message' => null,
];
}
$expiration = Carbon::parse($expirationDate);
$now = Carbon::now();
// Ensure whole day difference (no fractions)
$daysLeft = (int) floor($now->diffInDays($expiration, false));
// Define thresholds
$warning = $daysLeft <= 60 && $daysLeft >= 0;
$level = 'normal';
$message = null;
if ($daysLeft < 0) {
$level = 'expired';
$message = "❌ Your domain expired on {$expiration->format('Y-m-d')}. Please renew immediately!";
} elseif ($daysLeft <= 7) {
$level = 'critical';
$message = "⚠️ Domain expiring very soon! Only {$daysLeft} days left (expires {$expiration->format('Y-m-d')}).";
} elseif ($daysLeft <= 30) {
$level = 'warning';
$message = "⚠️ Please renew your domain soon. {$daysLeft} days remaining (expires {$expiration->format('Y-m-d')}).";
} elseif ($daysLeft <= 60) {
$level = 'notice';
$message = " Your domain will expire in {$daysLeft} days on {$expiration->format('Y-m-d')}.";
}
return [
'warning' => $warning,
'days_left' => $daysLeft,
'level' => $level,
'message' => $message,
];
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Helper;
use Illuminate\Support\Facades\DB;
use Modules\Restaurant\Models\Ingredient;
use Modules\Restaurant\Models\IngredientDamage;
use Modules\Restaurant\Models\Stock;
class IngredientDamageHelper
{
public static function createDamage(array $data)
{
DB::beginTransaction();
try {
// 1⃣ Create ingredient damage record
$damage = IngredientDamage::create($data);
// 2⃣ Reduce ingredient stock
$ingredient = Ingredient::find($data['ingredient_id']);
if (! $ingredient) {
throw new \Exception('Ingredient not found.');
}
$ingredient->decrement('stock_quantity', $data['quantity']);
// 3⃣ Log stock movement
Stock::create([
'restaurant_id' => $data['restaurant_id'],
'ingredient_id' => $data['ingredient_id'],
'type' => 'damage',
'quantity' => $data['quantity'],
'unit_cost' => $data['unit_cost'] ?? null,
'total_cost' => isset($data['unit_cost']) ? $data['unit_cost'] * $data['quantity'] : null,
'reference_type' => 'IngredientDamage',
'purchase_id' => $data['purchase_id'],
'batch_no' => $data['batch_no'] ?? null,
'expiry_date' => null,
'added_by' => $data['reported_by'] ?? auth()->id(),
'remarks' => $data['reason'] ?? 'Ingredient damage',
]);
DB::commit();
return $damage;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Helper;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Modules\Restaurant\Models\Order;
class OrderHelper
{
/**
* Generate a unique order number for the restaurant.
*
* Format: ORD-{YYYYMMDD}-{RestaurantId}-{AutoIncrement}
*/
public static function generateOrderNumber(int $restaurantId): string
{
$today = Carbon::now()->format('Ymd');
// Fetch last order (including soft-deleted)
$lastOrder = DB::table('orders')
->where('restaurant_id', $restaurantId)
->whereDate('created_at', Carbon::today())
->orderByDesc('id')
->first();
$lastIncrement = 0;
if ($lastOrder && preg_match('/ORD-\d{8}-\d+-([\d]+)/', $lastOrder->order_number, $matches)) {
$lastIncrement = (int) $matches[1];
}
$newIncrement = $lastIncrement + 1;
// Ensure unique, retry if needed (handles rare race conditions)
do {
$orderNumber = sprintf('ORD-%s-%d-%04d', $today, $restaurantId, $newIncrement);
$exists = Order::withTrashed()->where('order_number', $orderNumber)->exists();
if ($exists) {
$newIncrement++;
}
} while ($exists);
return $orderNumber;
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Helper;
use Carbon\Carbon;
use Modules\Restaurant\Models\Purchase;
class PurchaseHelper
{
/**
* Generate a unique purchase invoice number for a restaurant.
*
* Format: PUR-{YYYYMMDD}-{RestaurantId}-{AutoIncrement}
*/
public static function generateInvoiceNumber(int $restaurantId): string
{
$today = Carbon::now()->format('Ymd');
// Get last purchase today for this restaurant
$lastPurchase = Purchase::where('restaurant_id', $restaurantId)
->whereDate('created_at', Carbon::today())
->lockForUpdate() // prevent concurrency issues
->orderBy('id', 'desc')
->first();
$lastIncrement = 0;
if ($lastPurchase && preg_match('/PUR-\d{8}-\d+-([\d]+)/', $lastPurchase->invoice_no, $matches)) {
$lastIncrement = (int) $matches[1];
}
$newIncrement = $lastIncrement + 1;
return "PUR-{$today}-{$restaurantId}-".str_pad($newIncrement, 4, '0', STR_PAD_LEFT);
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace App\Helper;
use Illuminate\Support\Facades\DB;
use Modules\Restaurant\Models\PurchaseItem;
use Modules\Restaurant\Models\PurchaseReturn;
use Modules\Restaurant\Models\Stock;
class PurchaseReturnHelper
{
/**
* Process a return for purchase items.
*
* @param array $returnItems [ ['purchase_item_id'=>1,'quantity'=>2,'reason'=>'Damaged'], ... ]
*/
public static function processReturn(int $purchaseId, array $returnItems, ?int $userId = null): array
{
$results = [];
DB::beginTransaction();
try {
foreach ($returnItems as $item) {
$purchaseItem = PurchaseItem::lockForUpdate()->findOrFail($item['purchase_item_id']);
$returnQty = (float) $item['quantity'];
$availableQty = $purchaseItem->received_quantity - $purchaseItem->returned_quantity;
if ($returnQty <= 0 || $returnQty > $availableQty) {
throw new \Exception("Invalid return quantity for ingredient ID {$purchaseItem->ingredient_id}");
}
// 1⃣ Log return in purchase_returns table
$unitCost = $purchaseItem->unit_price;
$totalCost = $unitCost * $returnQty;
$purchaseReturn = PurchaseReturn::create([
'restaurant_id' => $purchaseItem->restaurant_id,
'purchase_id' => $purchaseItem->purchase_id,
'purchase_item_id' => $purchaseItem->id,
'ingredient_id' => $purchaseItem->ingredient_id,
'quantity' => $returnQty,
'unit_cost' => $unitCost,
'total_cost' => $totalCost,
'reason' => $item['reason'] ?? 'Purchase item return',
'processed_by' => $userId,
]);
// 2⃣ Update purchase_items returned_quantity
$purchaseItem->increment('returned_quantity', $returnQty);
// 3⃣ Update stock: decrease since returned to supplier
Stock::create([
'restaurant_id' => $purchaseItem->restaurant_id,
'ingredient_id' => $purchaseItem->ingredient_id,
'type' => 'return',
'quantity' => $returnQty,
'unit_cost' => $unitCost,
'total_cost' => $totalCost,
'reference_type' => 'PurchaseReturn',
'purchase_id' => $purchaseItem->purchase_id,
'added_by' => $userId,
'remarks' => $item['reason'] ?? 'Purchase item return',
]);
// 4⃣ Update ingredient stock quantity
$purchaseItem->ingredient->decrement('stock_quantity', $returnQty);
$results[] = [
'purchase_return_id' => $purchaseReturn->id,
'purchase_item_id' => $purchaseItem->id,
'ingredient_id' => $purchaseItem->ingredient_id,
'returned_quantity' => $returnQty,
];
}
DB::commit();
return ['status' => true, 'message' => 'Return processed successfully.', 'data' => $results];
} catch (\Exception $e) {
DB::rollBack();
return ['status' => false, 'message' => $e->getMessage(), 'data' => []];
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Helper;
use Illuminate\Support\Facades\DB;
class RestaurantHelper
{
/**
* Get restaurant ID based on the domain.
*
* @return mixed
*/
public static function getRestaurantIdByDomain(string $domain)
{
// Check if domain is provided
if (empty($domain)) {
return [
'error' => 'Domain is required.',
'status' => false,
];
}
// Check if domain format is valid (e.g., example.com)
if (! filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
return [
'error' => 'Invalid domain format.',
'status' => false,
];
}
// Fetch the restaurant based on the domain
$restaurant = DB::table('restaurants')
->where('domain', $domain)
->select('id')
->first();
// If no matching restaurant is found
if (! $restaurant) {
return [
'error' => 'Restaurant not found for the provided domain.',
'status' => false,
];
}
// Return the restaurant ID
return [
'restaurant_id' => $restaurant->id,
'status' => true,
];
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Helper;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Modules\Restaurant\Models\Ingredient;
use Modules\Restaurant\Models\Stock;
class StockHelper
{
/**
* Update ingredient stock and log the movement.
*
* @param string $type (purchase, usage, return, damage, transfer_in, transfer_out)
* @param float|null $unitCost (cost per unit)
* @param string|null $refType (e.g., Purchase, Order, Wastage)
*/
public static function updateStock(
int $ingredientId,
float $quantity,
string $type,
?float $unitCost = null,
?string $refType = null,
?int $refId = null,
?string $batchNo = null,
?string $expiryDate = null,
?int $restaurantId = null,
?int $addedBy = null
): void {
$ingredient = Ingredient::find($ingredientId);
if (! $ingredient) {
return;
}
// Determine signed quantity based on type
$signedQty = in_array($type, ['purchase', 'return', 'transfer_in', 'adjust_plus'])
? $quantity
: -$quantity;
// Update ingredient stock
$ingredient->increment('stock_quantity', $signedQty);
// Calculate total cost if unit cost is provided
$totalCost = $unitCost !== null ? $unitCost * $quantity : null;
// Record in stocks table
Stock::create([
'restaurant_id' => $restaurantId ?? $ingredient->restaurant_id ?? null,
'ingredient_id' => $ingredientId,
'type' => $type,
'quantity' => $quantity,
'unit_cost' => $unitCost,
'total_cost' => $totalCost,
'reference_type' => $refType,
'purchase_id' => $refId,
'batch_no' => $batchNo,
'expiry_date' => $expiryDate,
'added_by' => $addedBy ?? Auth::id(),
'remarks' => ucfirst($type).' stock movement',
]);
}
}

View File

@@ -0,0 +1,345 @@
<?php
use Carbon\Carbon;
use Illuminate\Mail\MailManager;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
use Modules\Authentication\Models\Setting;
use Modules\Authentication\Models\SubscriptionItem;
use Modules\Authentication\Models\User;
use Modules\HRM\Models\Attendance;
use Modules\HRM\Models\AttendanceLog;
use Modules\Restaurant\Models\FoodVariant;
if (! function_exists('_lang')) {
function _lang($string = '')
{
$targetLang = ! empty(Session::get('locale'))
? Session::get('locale')
: (! empty(get_option('language')) ? get_option('language') : 'en');
$json = [];
if (file_exists(lang_path()."/$targetLang.json")) {
$json = json_decode(file_get_contents(lang_path()."/$targetLang.json"), true);
}
return ! empty($json[$string]) ? $json[$string] : $string;
}
}
if (! function_exists('get_option')) {
function get_option($name)
{
try {
// Check if the database connection is valid
DB::connection()->getPdo();
$setting = DB::table('settings')->where('option_key', $name)->get();
if (! $setting->isEmpty()) {
return $setting[0]->option_value;
}
} catch (\Exception $e) {
// If database connection fails, return an empty string or log the error
return '';
}
}
}
if (! function_exists('get_saas_option')) {
function get_saas_option(string $name, $default = '')
{
try {
// Check DB connection
DB::connection()->getPdo();
// Ensure table exists
if (! Schema::hasTable('s_a_a_s_settings')) {
return $default;
}
// Fetch setting
$setting = DB::table('s_a_a_s_settings')
->where('name', $name)
->first();
return $setting ? $setting->value : $default;
} catch (\Throwable $e) {
return $default;
}
}
}
if (! function_exists('setEnvValue')) {
function setEnvValue(string $key, string $value): void
{
$envPath = base_path('.env');
if (! file_exists($envPath)) {
return;
}
$escapedValue = '"'.trim(str_replace('"', '\"', $value)).'"';
$envContent = file_get_contents($envPath);
if (preg_match("/^{$key}=.*/m", $envContent)) {
$envContent = preg_replace(
"/^{$key}=.*/m",
"{$key}={$escapedValue}",
$envContent
);
} else {
$envContent .= PHP_EOL."{$key}={$escapedValue}";
}
file_put_contents($envPath, $envContent);
}
}
if (! function_exists('fileUploader')) {
function fileUploader(string $dir, string $format, $image = null, $oldImage = null)
{
if ($image == null) {
return $oldImage ?? 'def.png';
}
// Delete old image(s) if exist
if (! empty($oldImage)) {
if (is_array($oldImage)) {
foreach ($oldImage as $file) {
Storage::disk('public')->delete($dir.$file);
}
} elseif (is_string($oldImage)) {
Storage::disk('public')->delete($dir.$oldImage);
}
}
// Generate unique file name
$imageName = Carbon::now()->toDateString().'-'.uniqid().'.'.$format;
// Ensure directory exists
if (! Storage::disk('public')->exists($dir)) {
Storage::disk('public')->makeDirectory($dir);
}
// Store the uploaded file
if ($image instanceof \Illuminate\Http\UploadedFile) {
$image->storeAs($dir, $imageName, 'public');
} else {
throw new \Exception('Invalid file type provided for upload.');
}
return $imageName;
}
}
if (! function_exists('fileRemover')) {
function fileRemover(string $dir, $image)
{
if (! isset($image)) {
return true;
}
if (Storage::disk('public')->exists($dir.$image)) {
Storage::disk('public')->delete($dir.$image);
}
return true;
}
}
if (! function_exists('restaurantUniqueRule')) {
function restaurantUniqueRule(string $table, string $column = 'name', ?int $restaurantId = null, ?int $ignoreId = null, string $restaurantColumn = 'restaurant_id')
{
$restaurantId = $restaurantId ?? auth()->user()->restaurant_id;
$rule = \Illuminate\Validation\Rule::unique($table, $column)
->where($restaurantColumn, $restaurantId);
if ($ignoreId) {
$rule->ignore($ignoreId);
}
return $rule;
}
}
if (! function_exists('restaurantUniqueRule')) {
function getDefaultVariant($foodId)
{
return FoodVariant::where('food_item_id', $foodId)->where('is_default', true)->first();
}
}
if (! function_exists('getUserRestaurantId')) {
function getUserRestaurantId()
{
$user = auth()->user();
if (! $user) {
return null;
}
return (int) $user->restaurant_id;
}
}
if (! function_exists('getUserId')) {
function getUserId()
{
$user = auth()->user();
if (! $user) {
return null;
}
return (int) $user->id;
}
}
if (! function_exists('authUser')) {
function authUser(): User
{
$authUser = auth()->user();
return $authUser;
}
}
if (! function_exists('updateAttendanceSummary')) {
function updateAttendanceSummary($employeeId, $date)
{
$attendance = Attendance::firstOrCreate([
'employee_id' => $employeeId,
'date' => $date,
]);
$logs = AttendanceLog::where('employee_id', $employeeId)
->whereDate('punch_time', $date)
->orderBy('punch_time')
->get();
if ($logs->isEmpty()) {
$attendance->update([
'first_clock_in' => null,
'last_clock_out' => null,
'hours_worked' => 0,
]);
return;
}
$firstIn = $logs->where('type', 'in')->first()?->punch_time;
$lastOut = $logs->where('type', 'out')->last()?->punch_time;
$totalSeconds = 0;
$stack = [];
foreach ($logs as $log) {
if ($log->type === 'in') {
$stack[] = $log->punch_time;
} elseif ($log->type === 'out' && ! empty($stack)) {
$in = array_pop($stack);
$out = $log->punch_time;
$totalSeconds += Carbon::parse($out)->diffInSeconds(Carbon::parse($in));
}
}
$hoursWorked = round($totalSeconds / 3600, 2);
$attendance->update([
'first_clock_in' => $firstIn ? Carbon::parse($firstIn)->format('H:i:s') : null,
'last_clock_out' => $lastOut ? Carbon::parse($lastOut)->format('H:i:s') : null,
'hours_worked' => $hoursWorked,
]);
}
}
if (! function_exists('checkPackageAccess')) {
function checkPackageAccess()
{
$subscription = SubscriptionItem::where(['user_id' => Auth::user()->id, 'restaurant_id' => GetUserRestaurantId()])
->with('package')->latest('id')
->first();
return $subscription->package?->name;
}
}
if (! function_exists('get_setting')) {
function get_setting($key, $restaurantId = null, $type = 'vendor', $default = null)
{
$query = Setting::query();
if ($restaurantId) {
$query->where('restaurant_id', $restaurantId);
}
if ($type) {
$query->where('type', $type);
}
$setting = $query->where('key', $key)->first();
return $setting ? $setting->value : $default;
}
}
if (! function_exists('shop_mailer_send')) {
/**
* Send email via shop SMTP dynamically
*
* @param \Illuminate\Mail\Mailable $mailable
*/
function shop_mailer_send(int $restaurantId, string $toEmail, $mailable): array
{
// 1. Check if default contact method is email
// $defaultMethod = get_setting('default_contact_method', $restaurantId);
// if ($defaultMethod !== 'email') {
// return [
// 'status' => false,
// 'message' => 'Default contact method is not email.',
// ];
// }
// 2. Get SMTP settings from database
$smtp = [
'transport' => 'smtp',
'host' => get_setting('email_smtp_host', $restaurantId),
'port' => get_setting('email_smtp_port', $restaurantId),
'encryption' => get_setting('email_smtp_encryption', $restaurantId) ?? 'tls',
'username' => get_setting('email_smtp_username', $restaurantId),
'password' => get_setting('email_smtp_password', $restaurantId),
'timeout' => null,
'local_domain' => parse_url(env('APP_URL', 'http://localhost'), PHP_URL_HOST),
];
// 3. Ensure all required credentials exist
if (empty($smtp['host']) || empty($smtp['port']) || empty($smtp['username']) || empty($smtp['password'])) {
return [
'status' => false,
'message' => 'SMTP configuration incomplete for this shop.',
];
}
try {
// 4. Dynamically set mailer config
config()->set('mail.mailers.dynamic', $smtp);
// 5. Get dynamic mailer instance
$mailer = app(MailManager::class)->mailer('dynamic');
// 6. Send email
$mailer->to($toEmail)->send($mailable);
return [
'status' => true,
'message' => 'Email sent successfully.',
];
} catch (Exception $e) {
return [
'status' => false,
'message' => 'Failed to send email: '.$e->getMessage(),
];
}
}
}