classify admin
This commit is contained in:
148
app/Services/BootstrapTableService.php
Normal file
148
app/Services/BootstrapTableService.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class BootstrapTableService
|
||||
{
|
||||
private static string $defaultClasses = 'btn icon btn-xs btn-rounded btn-icon rounded-pill';
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public static function button(string $iconClass, string $url, array $customClass = [], array $customAttributes = [], string $iconText = '')
|
||||
{
|
||||
$customClassStr = implode(' ', $customClass);
|
||||
$class = self::$defaultClasses . ' ' . $customClassStr;
|
||||
$attributes = '';
|
||||
if (count($customAttributes) > 0) {
|
||||
foreach ($customAttributes as $key => $value) {
|
||||
$attributes .= $key . '="' . $value . '" ';
|
||||
}
|
||||
}
|
||||
|
||||
return '<a href="' . $url . '" class="' . $class . '" ' . $attributes . '><i class="' . $iconClass . '"></i>' . $iconText . '</a> ';
|
||||
}
|
||||
|
||||
public static function dropdown(
|
||||
string $iconClass,
|
||||
array $dropdownItems,
|
||||
array $customClass = [],
|
||||
array $customAttributes = []
|
||||
) {
|
||||
$customClassStr = implode(' ', $customClass);
|
||||
$class = self::$defaultClasses . ' dropdown ' . $customClassStr;
|
||||
$attributes = '';
|
||||
|
||||
if (count($customAttributes) > 0) {
|
||||
foreach ($customAttributes as $key => $value) {
|
||||
$attributes .= $key . '="' . $value . '" ';
|
||||
}
|
||||
}
|
||||
|
||||
$dropdown = '<div class="' . $class . '" ' . $attributes . '>';
|
||||
$dropdown .= '<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-expanded="false">';
|
||||
$dropdown .= '<i class="' . $iconClass . '"></i>'; // Use the icon class here
|
||||
$dropdown .= '</button>';
|
||||
$dropdown .= '<ul class="dropdown-menu" data-bs-popper="static" aria-labelledby="dropdownMenuButton">';
|
||||
|
||||
foreach ($dropdownItems as $item) {
|
||||
$dropdown .= '<li><a class="dropdown-item" href="' . $item['url'] . '"><i class="' . $item['icon'] . '"></i> ' . $item['text'] . '</a></li>';
|
||||
}
|
||||
|
||||
$dropdown .= '</ul>';
|
||||
$dropdown .= '</div>';
|
||||
|
||||
return $dropdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $dataBsTarget
|
||||
* @param null $customClass
|
||||
* @param null $id
|
||||
* @param string $iconClass
|
||||
* @param null $onClick
|
||||
* @return string
|
||||
*/
|
||||
public static function editButton($url, bool $modal = false, $dataBsTarget = '#editModal', $customClass = null, $id = null, $iconClass = 'fa fa-edit', $onClick = null)
|
||||
{
|
||||
$customClass = ['btn-primary' . ' ' . $customClass];
|
||||
$customAttributes = [
|
||||
'title' => trans('Edit'),
|
||||
];
|
||||
if ($modal) {
|
||||
$customAttributes = [
|
||||
'title' => trans('Edit'),
|
||||
'data-bs-target' => $dataBsTarget,
|
||||
'data-bs-toggle' => 'modal',
|
||||
'id' => $id,
|
||||
'onclick' => $onClick,
|
||||
];
|
||||
|
||||
$customClass[] = 'edit_btn set-form-url';
|
||||
}
|
||||
|
||||
return self::button($iconClass, $url, $customClass, $customAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null $id
|
||||
* @param null $dataId
|
||||
* @param null $dataCategory
|
||||
* @param null $customClass
|
||||
* @return string
|
||||
*/
|
||||
public static function deleteButton($url, $id = null, $dataId = null, $dataCategory = null, $customClass = null)
|
||||
{
|
||||
// dd($dataId);
|
||||
$customClass = ['delete-form', 'btn-danger' . $customClass];
|
||||
$customAttributes = [
|
||||
'title' => trans('Delete'),
|
||||
'id' => $id,
|
||||
'data-id' => $dataId,
|
||||
'data-category' => $dataCategory,
|
||||
];
|
||||
$iconClass = 'fas fa-trash';
|
||||
|
||||
return self::button($iconClass, $url, $customClass, $customAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public static function restoreButton($url, string $title = 'Restore')
|
||||
{
|
||||
$customClass = ['btn-gradient-success', 'restore-data'];
|
||||
$customAttributes = [
|
||||
'title' => trans($title),
|
||||
];
|
||||
$iconClass = 'fa fa-refresh';
|
||||
|
||||
return self::button($iconClass, $url, $customClass, $customAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public static function trashButton($url)
|
||||
{
|
||||
$customClass = ['btn-gradient-danger', 'trash-data'];
|
||||
$customAttributes = [
|
||||
'title' => trans('Delete Permanent'),
|
||||
];
|
||||
$iconClass = 'fa fa-times';
|
||||
|
||||
return self::button($iconClass, $url, $customClass, $customAttributes);
|
||||
}
|
||||
|
||||
public static function optionButton($url)
|
||||
{
|
||||
$customClass = ['btn-option'];
|
||||
$customAttributes = [
|
||||
'title' => trans('View Option Data'),
|
||||
];
|
||||
$iconClass = 'bi bi-gear';
|
||||
$iconText = ' Options';
|
||||
|
||||
return self::button($iconClass, $url, $customClass, $customAttributes, $iconText);
|
||||
}
|
||||
}
|
||||
76
app/Services/CachingService.php
Normal file
76
app/Services/CachingService.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Language;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class CachingService
|
||||
{
|
||||
|
||||
/**
|
||||
* @param $key
|
||||
* @param callable $callback - Callback function must return a value
|
||||
* @param int $time = 3600
|
||||
* @return mixed
|
||||
*/
|
||||
public static function cacheRemember($key, callable $callback, int $time = 3600)
|
||||
{
|
||||
return Cache::remember($key, $time, $callback);
|
||||
}
|
||||
|
||||
public static function removeCache($key)
|
||||
{
|
||||
Cache::forget($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $key
|
||||
* @return mixed|string
|
||||
*/
|
||||
public static function getSystemSettings(array|string $key = '*')
|
||||
{
|
||||
$settings = self::cacheRemember(config('constants.CACHE.SETTINGS'), static function () {
|
||||
return Setting::all()->pluck('value', 'name');
|
||||
});
|
||||
|
||||
if (($key != '*')) {
|
||||
/* There is a minor possibility of getting a specific key from the $systemSettings
|
||||
* So I have not fetched Specific key from DB. Otherwise, Specific key will be fetched here
|
||||
* And it will be appended to the cached array here
|
||||
*/
|
||||
$specificSettings = [];
|
||||
|
||||
// If array is given in Key param
|
||||
if (is_array($key)) {
|
||||
foreach ($key as $row) {
|
||||
if ($settings && is_array($settings) && array_key_exists($row, $settings)) {
|
||||
$specificSettings[$row] = $settings[$row] ?? '';
|
||||
}
|
||||
}
|
||||
return $specificSettings;
|
||||
}
|
||||
|
||||
// If String is given in Key param
|
||||
if ($settings && is_object($settings) && $settings->has($key)) {
|
||||
return $settings[$key] ?? '';
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
return $settings;
|
||||
}
|
||||
|
||||
public static function getLanguages()
|
||||
{
|
||||
return self::cacheRemember(config('constants.CACHE.LANGUAGE'), static function () {
|
||||
return Language::all();
|
||||
});
|
||||
}
|
||||
|
||||
public static function getDefaultLanguage()
|
||||
{
|
||||
return Language::where('code', 'en')->first();
|
||||
}
|
||||
}
|
||||
57
app/Services/CurrencyFormatterService.php
Normal file
57
app/Services/CurrencyFormatterService.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Setting;
|
||||
|
||||
class CurrencyFormatterService
|
||||
{
|
||||
protected function resolveCurrency($currency): object
|
||||
{
|
||||
return (object) [
|
||||
'symbol' => $currency?->symbol ?? Setting::getValue('currency_symbol'),
|
||||
'symbol_position' => $currency?->symbol_position ?? Setting::getValue('currency_symbol_position'),
|
||||
'decimal_places' => $currency?->decimal_places ?? Setting::getValue('decimal_places'),
|
||||
'thousand_separator'=> $currency?->thousand_separator ?? Setting::getValue('thousand_separator'),
|
||||
'decimal_separator' => $currency?->decimal_separator ?? Setting::getValue('decimal_separator'),
|
||||
];
|
||||
}
|
||||
|
||||
public function formatPrice($amount, $currency = null): string
|
||||
{
|
||||
$currency = $this->resolveCurrency($currency);
|
||||
|
||||
$number = number_format(
|
||||
$amount,
|
||||
$currency->decimal_places,
|
||||
$currency->decimal_separator,
|
||||
$currency->thousand_separator
|
||||
);
|
||||
|
||||
$position = strtolower((string) $currency->symbol_position);
|
||||
|
||||
return $position === 'right'
|
||||
? $number. ' ' .$currency->symbol
|
||||
: $currency->symbol. ' ' .$number;
|
||||
}
|
||||
|
||||
public function formatSalaryRange($min, $max, $currency = null): ?string
|
||||
{
|
||||
if (! $min && ! $max) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($min && ! $max) {
|
||||
return __('From'). ' ' .$this->formatPrice($min, $currency);
|
||||
}
|
||||
|
||||
if (! $min && $max) {
|
||||
return __('Upto'). ' ' .$this->formatPrice($max, $currency);
|
||||
}
|
||||
|
||||
return
|
||||
$this->formatPrice($min, $currency)
|
||||
. ' - '
|
||||
. $this->formatPrice($max, $currency);
|
||||
}
|
||||
}
|
||||
106
app/Services/DefaultSettingService.php
Normal file
106
app/Services/DefaultSettingService.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class DefaultSettingService {
|
||||
|
||||
public static function get() {
|
||||
$SYSTEM_VERSION = "2.10.1";
|
||||
return [
|
||||
['name' => 'currency_symbol', 'value' => '$', 'type' => 'string'],
|
||||
['name' => 'ios_version', 'value' => '1.0.0', 'type' => 'string'],
|
||||
['name' => 'default_language', 'value' => 'en', 'type' => 'string'],
|
||||
['name' => 'force_update', 'value' => '0', 'type' => 'string'],
|
||||
['name' => 'android_version', 'value' => '1.0.0', 'type' => 'string'],
|
||||
['name' => 'number_with_suffix', 'value' => '0', 'type' => 'string'],
|
||||
['name' => 'maintenance_mode', 'value' => 0, 'type' => 'string'],
|
||||
['name' => 'privacy_policy', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'contact_us', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'terms_conditions', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'about_us', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'company_tel1', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'company_tel2', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'system_version', 'value' => $SYSTEM_VERSION, 'type' => 'string'],
|
||||
['name' => 'company_email', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'company_name', 'value' => 'Eclassify', 'type' => 'string'],
|
||||
['name' => 'company_logo', 'value' => 'assets/images/logo/sidebar_logo.png', 'type' => 'file'],
|
||||
['name' => 'favicon_icon', 'value' => 'assets/images/logo/favicon.png', 'type' => 'file'],
|
||||
['name' => 'login_image', 'value' => 'assets/images/bg/login.jpg', 'type' => 'file'],
|
||||
|
||||
['name' => 'banner_ad_id_android', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'banner_ad_id_ios', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'banner_ad_status', 'value' => '', 'type' => 'string'],
|
||||
|
||||
['name' => 'interstitial_ad_id_ios', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'interstitial_ad_id_android', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'interstitial_ad_status', 'value' => '', 'type' => 'string'],
|
||||
|
||||
['name' => 'pinterest_link', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'linkedin_link', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'facebook_link', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'x_link', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'instagram_link', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'google_map_iframe_link', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'app_store_link', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'play_store_link', 'value' => '', 'type' => 'string'],
|
||||
|
||||
['name' => 'footer_description', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'web_theme_color', 'value' => '#00B2CA', 'type' => 'string'],
|
||||
['name' => 'firebase_project_id', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'company_address', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'place_api_key', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'placeholder_image', 'value' => 'assets/images/logo/placeholder.png', 'type' => 'file'],
|
||||
['name' => 'header_logo', 'value' => 'assets/images/logo/Header Logo.svg', 'type' => 'file'],
|
||||
['name' => 'footer_logo', 'value' => 'assets/images/logo/Footer Logo.svg', 'type' => 'file'],
|
||||
['name' => 'default_latitude', 'value' => '-23.2420', 'type' => 'string'],
|
||||
['name' => 'default_longitude', 'value' => '-69.6669', 'type' => 'string'],
|
||||
['name' => 'file_manager', 'value' => 'public', 'type' => 'string'],
|
||||
['name' => 'show_landing_page', 'value' => '1', 'type' => 'boolean'],
|
||||
['name' => 'mobile_authentication', 'value' => '1', 'type' => 'boolean'],
|
||||
['name' => 'google_authentication', 'value' => '1', 'type' => 'boolean'],
|
||||
['name' => 'email_authentication', 'value' => '1', 'type' => 'boolean'],
|
||||
['name' => 'min_length', 'value' => '5', 'type' => 'number'],
|
||||
['name' => 'max_length', 'value' => '100', 'type' => 'number'],
|
||||
['name' => 'currency_symbol_position', 'value' => 'right', 'type' => 'string'],
|
||||
['name' => 'free_ad_listing', 'value' => '0', 'type' => 'boolean'],
|
||||
['name' => 'auto_approve_item', 'value' => '0', 'type' => 'boolean'],
|
||||
['name' => 'auto_approve_edited_item', 'value' => '0', 'type' => 'boolean'],
|
||||
['name' => 'mail_mailer', 'value' => 'smtp', 'type' => 'string'],
|
||||
['name' => 'mail_host', 'value' => 'mailhog', 'type' => 'string'],
|
||||
['name' => 'mail_port', 'value' => '1025', 'type' => 'string'],
|
||||
['name' => 'mail_username', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'mail_password', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'mail_encryption', 'value' => 'tls', 'type' => 'string'],
|
||||
['name' => 'mail_from_address', 'value' => 'hello@example.com', 'type' => 'string'],
|
||||
['name' => 'depp_link_scheme', 'value' => 'eclassify', 'type' => 'string'],
|
||||
['name' => 'otp_service_provider', 'value' => 'firebase', 'type' => 'string'],
|
||||
['name' => 'account_holder_name', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'bank_name', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'account_number', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'ifsc_swift_code', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'twilio_account_sid', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'twilio_auth_token', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'twilio_my_phone_number', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'map_provider', 'value' => 'free_api', 'type' => 'string'],
|
||||
['name' => 'refund_policy', 'value' => '', 'type' => 'string'],
|
||||
['name' => 'free_ad_unlimited', 'value' => '1', 'type' => 'boolean'],
|
||||
['name' => 'watermark_enabled', 'value' => '0', 'type' => 'boolean'],
|
||||
['name' => 'watermark_image', 'value' => '', 'type' => 'file'],
|
||||
['name' => 'watermark_opacity', 'value' => '50', 'type' => 'string'],
|
||||
['name' => 'watermark_size', 'value' => '20', 'type' => 'string'],
|
||||
['name' => 'watermark_style', 'value' => 'single', 'type' => 'string'],
|
||||
['name' => 'watermark_position', 'value' => 'center', 'type' => 'string'],
|
||||
['name' => 'watermark_rotation', 'value' => '-30', 'type' => 'string'],
|
||||
|
||||
['name' => 'currency_iso_code', 'value' => 'USD', 'type' => 'string'],
|
||||
['name' => 'currency_symbol_position', 'value' => 'left', 'type' => 'string'],
|
||||
|
||||
// --- Currency Formatting Settings ---
|
||||
['name' => 'decimal_places', 'value' => '2', 'type' => 'integer'],
|
||||
['name' => 'thousand_separator', 'value' => ',', 'type' => 'string'],
|
||||
['name' => 'decimal_separator', 'value' => '.', 'type' => 'string'],
|
||||
|
||||
];
|
||||
|
||||
}
|
||||
}
|
||||
163
app/Services/ExpiringItemService.php
Normal file
163
app/Services/ExpiringItemService.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Item;
|
||||
use App\Models\UserPurchasedPackage;
|
||||
use App\Models\User;
|
||||
use App\Models\UserFcmToken;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class ExpiringItemService
|
||||
{
|
||||
/**
|
||||
* Send notifications for expiring items and packages.
|
||||
*/
|
||||
public function notifyExpiringItemsAndPackages()
|
||||
{
|
||||
$this->notifyExpiringItems();
|
||||
$this->notifyExpiringPackages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notifications for expiring items.
|
||||
*/
|
||||
public function notifyExpiringItems()
|
||||
{
|
||||
$twoDaysFromNow = Carbon::now()->addDays(2)->startOfDay();
|
||||
$items = Item::whereDate('expiry_date', '=', $twoDaysFromNow)
|
||||
->where('status', 'approved')
|
||||
->get();
|
||||
$skippedCount = 0;
|
||||
|
||||
foreach ($items as $item) {
|
||||
$user = $item->user;
|
||||
if ($user && $user->email) {
|
||||
$this->sendNotification($user, $item);
|
||||
} else {
|
||||
$skippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($skippedCount > 0) {
|
||||
Log::warning("Skipped {$skippedCount} item notifications due to missing user or email");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notifications for expiring packages.
|
||||
*/
|
||||
public function notifyExpiringPackages()
|
||||
{
|
||||
$twoDaysFromNow = Carbon::now()->addDays(2)->startOfDay();
|
||||
$packages = UserPurchasedPackage::whereDate('end_date', '=', $twoDaysFromNow)->get();
|
||||
$skippedCount = 0;
|
||||
|
||||
foreach ($packages as $package) {
|
||||
$user = $package->user;
|
||||
$packageDetails = $package->package;
|
||||
|
||||
if ($user && $user->email && $packageDetails) {
|
||||
$this->sendPackageNotification($user, $packageDetails, $package);
|
||||
} else {
|
||||
$skippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($skippedCount > 0) {
|
||||
Log::warning("Skipped {$skippedCount} package notifications due to missing user, email, or package details");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email + push notification for expiring items.
|
||||
*/
|
||||
protected function sendNotification(User $user, $item)
|
||||
{
|
||||
try {
|
||||
$expiryDate = Carbon::parse($item->expiry_date)->format('d M Y');
|
||||
$message = "Your advertisement '{$item->name}' is expiring on {$expiryDate}. Please take action before it expires.";
|
||||
|
||||
// ✅ Send Email
|
||||
Mail::raw(
|
||||
"Hello {$user->name},\n\n{$message}",
|
||||
function ($msg) use ($user) {
|
||||
$msg->to($user->email)
|
||||
->from('admin@yourdomain.com', 'Admin')
|
||||
->subject('Advertisement Expiring Soon');
|
||||
}
|
||||
);
|
||||
|
||||
// ✅ Send Push Notification
|
||||
$user_tokens = UserFcmToken::where('user_id', $user->id)->pluck('fcm_token')->toArray();
|
||||
|
||||
if (!empty($user_tokens)) {
|
||||
$fcmMsg = [
|
||||
'item_id' => $item->id,
|
||||
'type' => 'item_expiry',
|
||||
'expiry_date' => $item->expiry_date,
|
||||
];
|
||||
|
||||
NotificationService::sendFcmNotification(
|
||||
$user_tokens,
|
||||
'Advertisement Expiring Soon',
|
||||
$message,
|
||||
'item_expiry',
|
||||
$fcmMsg
|
||||
);
|
||||
}
|
||||
|
||||
Log::info("Expiry notification sent to: {$user->email} for Advertisement: {$item->name}");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to send notification for Advertisement {$item->id}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send email + push notification for expiring packages.
|
||||
*/
|
||||
protected function sendPackageNotification(User $user, $package, $userPackage)
|
||||
{
|
||||
try {
|
||||
$expiryDate = Carbon::parse($userPackage->end_date)->format('d M Y');
|
||||
$message = "Your subscription package '{$package->name}' is expiring on {$expiryDate}. Please renew or upgrade your subscription.";
|
||||
|
||||
// ✅ Send Email
|
||||
Mail::raw(
|
||||
"Hello {$user->name},\n\n{$message}",
|
||||
function ($msg) use ($user) {
|
||||
$msg->to($user->email)
|
||||
->from('admin@yourdomain.com', 'Admin')
|
||||
->subject('Package Expiring Soon');
|
||||
}
|
||||
);
|
||||
|
||||
// ✅ Send Push Notification
|
||||
$user_tokens = UserFcmToken::where('user_id', $user->id)->pluck('fcm_token')->toArray();
|
||||
|
||||
if (!empty($user_tokens)) {
|
||||
$fcmMsg = [
|
||||
'package_id' => $package->id,
|
||||
'type' => 'package_expiry',
|
||||
'expiry_date' => $userPackage->end_date,
|
||||
];
|
||||
|
||||
NotificationService::sendFcmNotification(
|
||||
$user_tokens,
|
||||
'Package Expiring Soon',
|
||||
$message,
|
||||
'package_expiry',
|
||||
$fcmMsg
|
||||
);
|
||||
}
|
||||
|
||||
Log::info("Package expiry notification sent to: {$user->email} for package: {$package->name}");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to send notification for Package {$userPackage->id}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
286
app/Services/FileService.php
Normal file
286
app/Services/FileService.php
Normal file
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Jobs\AddWatermarkJob;
|
||||
use App\Models\Setting;
|
||||
use App\Services\HelperService;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Facades\Image;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use RuntimeException;
|
||||
use Spatie\ImageOptimizer\OptimizerChainFactory;
|
||||
|
||||
class FileService
|
||||
{
|
||||
/**
|
||||
* @param $requestFile
|
||||
* @param string $folder
|
||||
* @param bool $addWaterMark
|
||||
* @return string|false
|
||||
*/
|
||||
public static function compressAndUpload($requestFile, string $folder, bool $addWaterMark = false)
|
||||
{
|
||||
$filenameWithoutExt = pathinfo($requestFile->getClientOriginalName(), PATHINFO_FILENAME);
|
||||
$extension = strtolower($requestFile->getClientOriginalExtension());
|
||||
$fileName = time() . '-' . Str::slug($filenameWithoutExt) . '.' . $extension;
|
||||
$disk = config('filesystems.default');
|
||||
$path = $folder . '/' . $fileName;
|
||||
|
||||
try {
|
||||
if (in_array($extension, ['jpg', 'jpeg', 'png', 'webp'])) {
|
||||
// Compress and save image
|
||||
$image = Image::make($requestFile)->encode($extension, 80);
|
||||
Storage::disk($disk)->put($path, (string) $image);
|
||||
|
||||
// Get absolute path safely
|
||||
$absolutePath = self::getAbsolutePath($disk, $path);
|
||||
if (! $absolutePath) {
|
||||
// Log::warning('Cannot get absolute path for image', ['disk' => $disk, 'path' => $path]);
|
||||
return $path;
|
||||
}
|
||||
// Queue watermark if enabled
|
||||
if ($addWaterMark && HelperService::getWatermarkConfigStatus()) {
|
||||
// Log::info('Watermark: watermark is enable');
|
||||
AddWatermarkJob::dispatch($absolutePath, $extension);
|
||||
} elseif ($addWaterMark) {
|
||||
Log::info('Watermark skipped: watermark is disabled');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
// Non-image files
|
||||
$requestFile->storeAs($folder, $fileName, $disk);
|
||||
return $path;
|
||||
} catch (Exception $e) {
|
||||
Log::error('FileService::compressAndUpload error: ' . $e->getMessage(), ['file' => $path]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute path for a file stored on a disk
|
||||
*
|
||||
* @param string $disk
|
||||
* @param string $path
|
||||
* @return string|null
|
||||
*/
|
||||
private static function getAbsolutePath(string $disk, string $path): ?string
|
||||
{
|
||||
try {
|
||||
$storagePath = Storage::disk($disk)->path($path);
|
||||
if (file_exists($storagePath)) {
|
||||
return $storagePath;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Log::error('FileService::getAbsolutePath error: ' . $e->getMessage(), ['disk' => $disk, 'path' => $path]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param $requestFile
|
||||
* @param $folder
|
||||
* @return string
|
||||
*/
|
||||
public static function upload($requestFile, $folder)
|
||||
{
|
||||
$file_name = uniqid('', true) . time() . '.' . $requestFile->getClientOriginalExtension();
|
||||
Storage::disk(config('filesystems.default'))->putFileAs($folder, $requestFile, $file_name);
|
||||
return $folder . '/' . $file_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $requestFile
|
||||
* @param $folder
|
||||
* @param $deleteRawOriginalImage
|
||||
* @return string
|
||||
*/
|
||||
public static function replace($requestFile, $folder, $deleteRawOriginalImage)
|
||||
{
|
||||
self::delete($deleteRawOriginalImage);
|
||||
return self::upload($requestFile, $folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $requestFile
|
||||
* @param $folder
|
||||
* @param $deleteRawOriginalImage
|
||||
* @return string
|
||||
*/
|
||||
public static function compressAndReplace($requestFile, $folder, $deleteRawOriginalImage, bool $addWaterMark = false)
|
||||
{
|
||||
if (!empty($deleteRawOriginalImage)) {
|
||||
self::delete($deleteRawOriginalImage);
|
||||
}
|
||||
return self::compressAndUpload($requestFile, $folder, $addWaterMark);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $requestFile
|
||||
* @param $code
|
||||
* @return string
|
||||
*/
|
||||
public static function uploadLanguageFile($requestFile, $code)
|
||||
{
|
||||
$filename = $code . '.' . $requestFile->getClientOriginalExtension();
|
||||
if (file_exists(base_path('resources/lang/') . $filename)) {
|
||||
File::delete(base_path('resources/lang/') . $filename);
|
||||
}
|
||||
$requestFile->move(base_path('resources/lang/'), $filename);
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $file
|
||||
* @return bool
|
||||
*/
|
||||
public static function deleteLanguageFile($file)
|
||||
{
|
||||
if (file_exists(base_path('resources/lang/') . $file)) {
|
||||
return File::delete(base_path('resources/lang/') . $file);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $image = rawOriginalPath
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete($image)
|
||||
{
|
||||
|
||||
if (!empty($image) && Storage::disk(config('filesystems.default'))->exists($image)) {
|
||||
return Storage::disk(config('filesystems.default'))->delete($image);
|
||||
}
|
||||
|
||||
//Image does not exist in server so feel free to upload new image
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function compressAndUploadWithWatermark($requestFile, $folder)
|
||||
{
|
||||
$file_name = uniqid('', true) . time() . '.' . $requestFile->getClientOriginalExtension();
|
||||
|
||||
try {
|
||||
if (in_array($requestFile->getClientOriginalExtension(), ['jpg', 'jpeg', 'png'])) {
|
||||
$watermarkPath = Setting::where('name', 'watermark_image')->value('value');
|
||||
|
||||
$fullWatermarkPath = storage_path('app/public/' . $watermarkPath);
|
||||
$watermark = null;
|
||||
|
||||
$imagePath = $requestFile->getPathname();
|
||||
if (!file_exists($imagePath) || !is_readable($imagePath)) {
|
||||
throw new RuntimeException("Uploaded image file is not readable at path: " . $imagePath);
|
||||
}
|
||||
$image = Image::make($imagePath)->encode(null, 60);
|
||||
$imageWidth = $image->width();
|
||||
$imageHeight = $image->height();
|
||||
|
||||
if (!empty($watermarkPath) && file_exists($fullWatermarkPath)) {
|
||||
$watermark = Image::make($fullWatermarkPath)
|
||||
->resize($imageWidth, $imageHeight, function ($constraint) {
|
||||
$constraint->aspectRatio(); // Preserve aspect ratio
|
||||
})
|
||||
->opacity(10);
|
||||
}
|
||||
|
||||
if ($watermark) {
|
||||
$image->insert($watermark, 'center');
|
||||
}
|
||||
|
||||
Storage::disk(config('filesystems.default'))->put($folder . '/' . $file_name, (string)$image->encode());
|
||||
} else {
|
||||
// Else assign file as it is
|
||||
$file = $requestFile;
|
||||
$file->storeAs($folder, $file_name, 'public');
|
||||
}
|
||||
return $folder . '/' . $file_name;
|
||||
} catch (Exception $e) {
|
||||
throw new RuntimeException($e);
|
||||
// $file = $requestFile;
|
||||
// return $file->storeAs($folder, $file_name, 'public');
|
||||
}
|
||||
}
|
||||
public static function compressAndReplaceWithWatermark($requestFile, $folder, $deleteRawOriginalImage = null)
|
||||
{
|
||||
|
||||
if (!empty($deleteRawOriginalImage)) {
|
||||
self::delete($deleteRawOriginalImage);
|
||||
}
|
||||
|
||||
$file_name = uniqid('', true) . time() . '.' . $requestFile->getClientOriginalExtension();
|
||||
|
||||
try {
|
||||
if (in_array($requestFile->getClientOriginalExtension(), ['jpg', 'jpeg', 'png'])) {
|
||||
$watermarkPath = Setting::where('name', 'watermark_image')->value('value');
|
||||
$fullWatermarkPath = storage_path('app/public/' . $watermarkPath);
|
||||
$watermark = null;
|
||||
$imagePath = $requestFile->getPathname();
|
||||
if (!file_exists($imagePath) || !is_readable($imagePath)) {
|
||||
throw new RuntimeException("Uploaded image file is not readable at path: " . $imagePath);
|
||||
}
|
||||
$image = Image::make($imagePath)->encode(null, 60);
|
||||
$imageWidth = $image->width();
|
||||
$imageHeight = $image->height();
|
||||
|
||||
|
||||
if (!empty($watermarkPath) && file_exists($fullWatermarkPath)) {
|
||||
$watermark = Image::make($fullWatermarkPath)
|
||||
->resize($imageWidth, $imageHeight, function ($constraint) {
|
||||
$constraint->aspectRatio(); // Preserve aspect ratio
|
||||
})
|
||||
->opacity(10);
|
||||
}
|
||||
|
||||
if ($watermark) {
|
||||
$image->insert($watermark, 'center');
|
||||
}
|
||||
|
||||
|
||||
Storage::disk(config('filesystems.default'))->put($folder . '/' . $file_name, (string)$image->encode());
|
||||
} else {
|
||||
|
||||
$file = $requestFile;
|
||||
$file->storeAs($folder, $file_name, 'public');
|
||||
}
|
||||
|
||||
return $folder . '/' . $file_name;
|
||||
} catch (Exception $e) {
|
||||
throw new RuntimeException($e);
|
||||
}
|
||||
}
|
||||
|
||||
public static function renameLanguageFiles(string $oldCode, string $newCode): void
|
||||
{
|
||||
$langPath = resource_path('lang');
|
||||
|
||||
// Rename JSON file (for frontend)
|
||||
if (file_exists($langPath . '/' . $oldCode . '.json')) {
|
||||
rename(
|
||||
$langPath . '/' . $oldCode . '.json',
|
||||
$langPath . '/' . $newCode . '.json'
|
||||
);
|
||||
}
|
||||
|
||||
// Rename PHP language folder (for backend)
|
||||
if (is_dir($langPath . '/' . $oldCode)) {
|
||||
rename(
|
||||
$langPath . '/' . $oldCode,
|
||||
$langPath . '/' . $newCode
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
624
app/Services/HelperService.php
Normal file
624
app/Services/HelperService.php
Normal file
@@ -0,0 +1,624 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Area;
|
||||
use App\Models\Category;
|
||||
use App\Models\Setting;
|
||||
use App\Services\CachingService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use JsonException;
|
||||
|
||||
class HelperService
|
||||
{
|
||||
public static function changeEnv($updateData = []): bool
|
||||
{
|
||||
if (count($updateData) > 0) {
|
||||
// Read .env-file
|
||||
$env = file_get_contents(base_path() . '/.env');
|
||||
// Split string on every " " and write into array
|
||||
// $env = explode(PHP_EOL, $env);
|
||||
$env = preg_split('/\r\n|\r|\n/', $env);
|
||||
$env_array = [];
|
||||
foreach ($env as $env_value) {
|
||||
if (empty($env_value)) {
|
||||
// Add and Empty Line
|
||||
$env_array[] = '';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$entry = explode('=', $env_value, 2);
|
||||
$env_array[$entry[0]] = $entry[0] . '="' . str_replace('"', '', $entry[1]) . '"';
|
||||
}
|
||||
|
||||
foreach ($updateData as $key => $value) {
|
||||
$env_array[$key] = $key . '="' . str_replace('"', '', $value) . '"';
|
||||
}
|
||||
// Turn the array back to a String
|
||||
$env = implode("\n", $env_array);
|
||||
|
||||
// And overwrite the .env with the new data
|
||||
file_put_contents(base_path() . '/.env', $env);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description - This function will return the nested category Option tags using in memory optimization
|
||||
*/
|
||||
public static function childCategoryRendering(&$categories, int $level = 0, ?string $parentCategoryID = ''): bool
|
||||
{
|
||||
// Foreach loop only on the parent category objects
|
||||
foreach (collect($categories)->where('parent_category_id', $parentCategoryID) as $key => $category) {
|
||||
echo "<option value='$category->id'>" . str_repeat(' ', $level * 4) . '|-- ' . $category->name . '</option>';
|
||||
// Once the parent category object is rendered we can remove the category from the main object so that redundant data can be removed
|
||||
$categories->forget($key);
|
||||
|
||||
// Now fetch the subcategories of the main category
|
||||
$subcategories = $categories->where('parent_category_id', $category->id);
|
||||
if (! empty($subcategories)) {
|
||||
// Finally if subcategories are available then call the recursive function & see the magic
|
||||
self::childCategoryRendering($categories, $level + 1, $category->id);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function buildNestedChildSubcategoryObject($categories)
|
||||
{
|
||||
// Used json_decode & encode simultaneously because i wanted to convert whole nested array into object
|
||||
try {
|
||||
return json_decode(json_encode(self::buildNestedChildSubcategoryArray($categories), JSON_THROW_ON_ERROR), false, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException) {
|
||||
return (object) [];
|
||||
}
|
||||
}
|
||||
|
||||
private static function buildNestedChildSubcategoryArray($categories)
|
||||
{
|
||||
$children = [];
|
||||
// First Add Parent Categories to root level in an array
|
||||
foreach ($categories->toArray() as $value) {
|
||||
if ($value['parent_category_id'] == '') {
|
||||
$children[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Then loop on the Parent Category to find the children categories
|
||||
foreach ($children as $key => $value) {
|
||||
$children[$key]['subcategories'] = self::findChildCategories($categories->toArray(), $value['id']);
|
||||
}
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
public static function findChildCategories($arr, $parent)
|
||||
{
|
||||
$children = [];
|
||||
foreach ($arr as $key => $value) {
|
||||
if ($value['parent_category_id'] == $parent) {
|
||||
$children[] = $value;
|
||||
}
|
||||
}
|
||||
foreach ($children as $key => $value) {
|
||||
$children[$key]['subcategories'] = self::findChildCategories($arr, $value['id']);
|
||||
}
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
/*
|
||||
* Sagar's Code :
|
||||
* in this i have approached the reverse object moving & removing.
|
||||
* which is not working as of now.
|
||||
* but will continue working on this in future as it seems bit optimized approach from the current one
|
||||
public static function buildNestedChildSubcategoryObject($categories, $finalCategories = []) {
|
||||
echo "<pre>";
|
||||
// Foreach loop only on the parent category objects
|
||||
if (!empty($finalCategories)) {
|
||||
$finalCategories = $categories->whereNull('parent_category_id');
|
||||
}
|
||||
foreach ($categories->whereNotNull('parent_category_id')->sortByDesc('parent_category_id') as $key => $category) {
|
||||
echo "----------------------------------------------------------------------<br>";
|
||||
$parentCategoryIndex = $categories->search(function ($data) use ($category) {
|
||||
return $data['id'] == $category->parent_category_id;
|
||||
});
|
||||
if (!$parentCategoryIndex) {
|
||||
continue;
|
||||
}
|
||||
// echo "*** This category will be moved to its parent category object ***<br>";
|
||||
// print_r($category->toArray());
|
||||
|
||||
// Once the parent category object is rendered we can remove the category from the main object so that redundant data can be removed
|
||||
$categories[$parentCategoryIndex]->subcategories[] = $category->toArray();
|
||||
|
||||
$categories->forget($key);
|
||||
echo "<br>*** After all the operation main categories object will look like this ***<br>";
|
||||
print_r($categories->toArray());
|
||||
|
||||
if (!empty($categories)) {
|
||||
// Finally if subcategories are available then call the recursive function & see the magic
|
||||
return self::buildNestedChildSubcategoryObject($categories, $finalCategories);
|
||||
}
|
||||
}
|
||||
return $categories;
|
||||
} */
|
||||
|
||||
public static function findParentCategory($category, $finalCategories = [])
|
||||
{
|
||||
$category = Category::find($category);
|
||||
|
||||
if (! empty($category)) {
|
||||
$finalCategories[] = $category->id;
|
||||
|
||||
if (! empty($category->parent_category_id)) {
|
||||
$finalCategories[] = self::findParentCategory($category->id, $finalCategories);
|
||||
}
|
||||
}
|
||||
|
||||
return $finalCategories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Slug for any model
|
||||
*
|
||||
* @param $model - Instance of Model
|
||||
*/
|
||||
public static function generateUniqueSlug($model, string $slug, ?int $excludeID = null, int $count = 0): string
|
||||
{
|
||||
/* NOTE : This can be improved by directly calling in the UI on type of title via AJAX */
|
||||
$slug = Str::slug($slug);
|
||||
$newSlug = $count ? $slug . '-' . $count : $slug;
|
||||
|
||||
$data = $model::where('slug', $newSlug);
|
||||
if ($excludeID !== null) {
|
||||
$data->where('id', '!=', $excludeID);
|
||||
}
|
||||
|
||||
if (in_array(SoftDeletes::class, class_uses_recursive($model), true)) {
|
||||
$data->withTrashed();
|
||||
}
|
||||
while ($data->exists()) {
|
||||
return self::generateUniqueSlug($model, $slug, $excludeID, $count + 1);
|
||||
}
|
||||
|
||||
return $newSlug;
|
||||
}
|
||||
|
||||
public static function findAllCategoryIds($model): array
|
||||
{
|
||||
$ids = [];
|
||||
|
||||
foreach ($model as $item) {
|
||||
$ids[] = $item['id'];
|
||||
|
||||
if (! empty($item['children'])) {
|
||||
$ids = array_merge($ids, self::findAllCategoryIds($item['children']));
|
||||
}
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
public static function generateRandomSlug($length = 10)
|
||||
{
|
||||
// Generate a random string of lowercase letters and numbers
|
||||
$characters = 'abcdefghijklmnopqrstuvwxyz-';
|
||||
$slug = '';
|
||||
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$index = rand(0, strlen($characters) - 1);
|
||||
$slug .= $characters[$index];
|
||||
}
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply location filters to Item query with fallback logic
|
||||
* Priority: area_id > city > state > country > latitude/longitude
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param callable $applyAuthFilters Function to apply auth-specific filters
|
||||
* @return array ['query' => $query, 'message' => $locationMessage]
|
||||
*/
|
||||
public static function applyLocationFilters($query, $request, $applyAuthFilters)
|
||||
{
|
||||
$isHomePage = $request->current_page === 'home';
|
||||
$locationMessage = null;
|
||||
$hasLocationFilter = $request->latitude !== null && $request->longitude !== null;
|
||||
$hasCityFilter = !empty($request->city);
|
||||
$hasStateFilter = !empty($request->state);
|
||||
$hasCountryFilter = !empty($request->country);
|
||||
$hasAreaFilter = !empty($request->area_id);
|
||||
$hasAreaLocationFilter = !empty($request->area_latitude) && !empty($request->area_longitude);
|
||||
|
||||
$cityName = $request->city ?? null;
|
||||
$stateName = $request->state ?? null;
|
||||
$countryName = $request->country ?? null;
|
||||
$areaId = $request->area_id ?? null;
|
||||
|
||||
$cityItemCount = 0;
|
||||
$stateItemCount = 0;
|
||||
$countryItemCount = 0;
|
||||
$areaItemCount = 0;
|
||||
$areaName = null;
|
||||
|
||||
// Handle area location filter (find closest area by lat/long)
|
||||
if ($hasAreaLocationFilter && !$hasAreaFilter) {
|
||||
$areaLat = $request->area_latitude;
|
||||
$areaLng = $request->area_longitude;
|
||||
$haversine = "(6371 * acos(cos(radians($areaLat))
|
||||
* cos(radians(latitude))
|
||||
* cos(radians(longitude) - radians($areaLng))
|
||||
+ sin(radians($areaLat)) * sin(radians(latitude))))";
|
||||
|
||||
$closestArea = Area::whereNotNull('latitude')
|
||||
->whereNotNull('longitude')
|
||||
->selectRaw("areas.*, {$haversine} AS distance")
|
||||
// ->orderBy('distance', 'asc')
|
||||
|
||||
->orderByRaw(
|
||||
'(6371 * acos(
|
||||
cos(radians(?)) *
|
||||
cos(radians(latitude)) *
|
||||
cos(radians(longitude) - radians(?)) +
|
||||
sin(radians(?)) * sin(radians(latitude))
|
||||
)) ASC',
|
||||
[$areaLat, $areaLng, $areaLat]
|
||||
)
|
||||
|
||||
->first();
|
||||
|
||||
if ($closestArea) {
|
||||
$hasAreaFilter = true;
|
||||
$areaId = $closestArea->id;
|
||||
}
|
||||
}
|
||||
|
||||
// Get area name if area filter is set
|
||||
if ($hasAreaFilter) {
|
||||
$area = Area::find($areaId);
|
||||
$areaName = $area ? $area->name : __('the selected area');
|
||||
}
|
||||
|
||||
// Save base query before location filters for fallback
|
||||
$baseQueryBeforeLocation = clone $query;
|
||||
|
||||
// First, check for area filter (highest priority)
|
||||
if ($hasAreaFilter) {
|
||||
$areaQuery = clone $query;
|
||||
$areaQuery->where('area_id', $areaId);
|
||||
$areaQuery = $applyAuthFilters($areaQuery);
|
||||
$areaItemExists = $areaQuery->exists();
|
||||
|
||||
if ($areaItemExists) {
|
||||
$query = $areaQuery;
|
||||
$areaItemCount = 1;
|
||||
} else {
|
||||
if ($isHomePage) {
|
||||
$locationMessage = __('No Ads found in :area. Showing all available Ads.', ['area' => $areaName]);
|
||||
} else {
|
||||
$query = $areaQuery;
|
||||
}
|
||||
$areaItemCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Second, check for city filter
|
||||
if ($hasCityFilter && (!$hasAreaFilter || $areaItemCount == 0)) {
|
||||
$cityQuery = clone $query;
|
||||
$cityQuery->where('city', $cityName);
|
||||
$cityQuery = $applyAuthFilters($cityQuery);
|
||||
$cityItemExists = $cityQuery->exists();
|
||||
|
||||
if ($cityItemExists) {
|
||||
$query = $cityQuery;
|
||||
$cityItemCount = 1;
|
||||
if ($hasAreaFilter && $areaItemCount == 0 && $isHomePage) {
|
||||
$locationMessage = __('No Ads found in :city. Showing all available Ads.', ['city' => $cityName]);
|
||||
}
|
||||
} else {
|
||||
$cityItemCount = 0;
|
||||
if ($isHomePage) {
|
||||
if (!$locationMessage) {
|
||||
$locationMessage = __('No Ads found in :city. Showing all available Ads.', ['city' => $cityName]);
|
||||
} else {
|
||||
$locationMessage = __('No Ads found in :area or :city. Showing all available Ads.', ['area' => $areaName, 'city' => $cityName]);
|
||||
}
|
||||
} else {
|
||||
$query = $cityQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Third, check for state filter
|
||||
if ($hasStateFilter && (!$hasAreaFilter || $areaItemCount == 0) && (!$hasCityFilter || $cityItemCount == 0)) {
|
||||
$stateQuery = clone $query;
|
||||
$stateQuery->where('state', $stateName);
|
||||
$stateQuery = $applyAuthFilters($stateQuery);
|
||||
$stateItemExists = $stateQuery->exists();
|
||||
|
||||
if ($stateItemExists) {
|
||||
$query = $stateQuery;
|
||||
$stateItemCount = 1;
|
||||
if (($hasAreaFilter && $areaItemCount == 0) || ($hasCityFilter && $cityItemCount == 0)) {
|
||||
if ($isHomePage) {
|
||||
$locationMessage = __('No Ads found in :state. Showing all available Ads.', ['state' => $stateName]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$stateItemCount = 0;
|
||||
if ($isHomePage) {
|
||||
if (!$locationMessage) {
|
||||
$locationMessage = __('No Ads found in :state. Showing all available Ads.', ['state' => $stateName]);
|
||||
} else {
|
||||
$parts = [];
|
||||
if ($hasAreaFilter && $areaItemCount == 0) {
|
||||
$parts[] = $areaName;
|
||||
}
|
||||
if ($hasCityFilter && $cityItemCount == 0) {
|
||||
$parts[] = $cityName;
|
||||
}
|
||||
$parts[] = $stateName;
|
||||
$locationMessage = __('No Ads found in :locations. Showing all available Ads.', ['locations' => implode(', ', $parts)]);
|
||||
}
|
||||
} else {
|
||||
$query = $stateQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fourth, check for country filter
|
||||
if ($hasCountryFilter && (!$hasAreaFilter || $areaItemCount == 0) && (!$hasCityFilter || $cityItemCount == 0) && (!$hasStateFilter || $stateItemCount == 0)) {
|
||||
$countryQuery = clone $query;
|
||||
$countryQuery->where('country', $countryName);
|
||||
$countryQuery = $applyAuthFilters($countryQuery);
|
||||
$countryItemExists = $countryQuery->exists();
|
||||
|
||||
if ($countryItemExists) {
|
||||
$query = $countryQuery;
|
||||
$countryItemCount = 1;
|
||||
if (($hasAreaFilter && $areaItemCount == 0) || ($hasCityFilter && $cityItemCount == 0) || ($hasStateFilter && $stateItemCount == 0)) {
|
||||
if ($isHomePage) {
|
||||
$locationMessage = __('No Ads found in :country. Showing all available Ads.', ['country' => $countryName]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$countryItemCount = 0;
|
||||
if ($isHomePage) {
|
||||
if (!$locationMessage) {
|
||||
$locationMessage = __('No Ads found in :country. Showing all available Ads.', ['country' => $countryName]);
|
||||
} else {
|
||||
$parts = [];
|
||||
if ($hasAreaFilter && $areaItemCount == 0) {
|
||||
$parts[] = $areaName;
|
||||
}
|
||||
if ($hasCityFilter && $cityItemCount == 0) {
|
||||
$parts[] = $cityName;
|
||||
}
|
||||
if ($hasStateFilter && $stateItemCount == 0) {
|
||||
$parts[] = $stateName;
|
||||
}
|
||||
$parts[] = $countryName;
|
||||
$locationMessage = __('No Ads found in :locations. Showing all available Ads.', ['locations' => implode(', ', $parts)]);
|
||||
}
|
||||
} else {
|
||||
$query = $countryQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fifth, handle latitude/longitude location-based search
|
||||
$hasHigherPriorityFilter = ($hasAreaFilter && $areaItemCount > 0) || ($hasCityFilter && $cityItemCount > 0) || ($hasStateFilter && $stateItemCount > 0) || ($hasCountryFilter && $countryItemCount > 0);
|
||||
if ($hasLocationFilter && ((!$hasAreaFilter && !$hasCityFilter && !$hasStateFilter && !$hasCountryFilter) || $hasHigherPriorityFilter)) {
|
||||
$latitude = $request->latitude;
|
||||
$longitude = $request->longitude;
|
||||
$requestedRadius = (float)($request->radius ?? null);
|
||||
$exactLocationRadius = $request->radius;
|
||||
|
||||
$haversine = '(6371 * acos(cos(radians(?))
|
||||
* cos(radians(latitude))
|
||||
* cos(radians(longitude) - radians(?))
|
||||
+ sin(radians(?)) * sin(radians(latitude))))';
|
||||
|
||||
$exactLocationQuery = clone $query;
|
||||
$exactLocationQuery
|
||||
->select('items.*')
|
||||
->selectRaw("$haversine AS distance", [$latitude, $longitude, $latitude])
|
||||
->where('latitude', '!=', 0)
|
||||
->where('longitude', '!=', 0)
|
||||
// CHANGE THIS: Use whereRaw instead of having to support pagination count
|
||||
// Use <= so radius=0.0 still returns items at the exact same coordinates (distance 0)
|
||||
->whereRaw("$haversine <= ?", [$latitude, $longitude, $latitude, $exactLocationRadius])
|
||||
->orderBy('distance', 'asc');
|
||||
|
||||
if (Auth::check()) {
|
||||
$exactLocationQuery->with(['item_offers' => function ($q) {
|
||||
$q->where('buyer_id', Auth::user()->id);
|
||||
}, 'user_reports' => function ($q) {
|
||||
$q->where('user_id', Auth::user()->id);
|
||||
}]);
|
||||
|
||||
$currentURI = explode('?', $request->getRequestUri(), 2);
|
||||
if ($currentURI[0] == '/api/my-items') {
|
||||
$exactLocationQuery->where(['user_id' => Auth::user()->id])->withTrashed();
|
||||
} else {
|
||||
$exactLocationQuery->where('status', 'approved')->has('user')->onlyNonBlockedUsers()->getNonExpiredItems();
|
||||
}
|
||||
} else {
|
||||
$exactLocationQuery->where('status', 'approved')->getNonExpiredItems();
|
||||
}
|
||||
|
||||
$exactLocationExists = $exactLocationQuery->exists();
|
||||
|
||||
if ($exactLocationExists) {
|
||||
$query = $exactLocationQuery;
|
||||
} else {
|
||||
$searchRadius = $requestedRadius !== null && $requestedRadius > 0 ? $requestedRadius : 50;
|
||||
|
||||
$nearbyQuery = clone $query;
|
||||
$nearbyQuery
|
||||
->select('items.*')
|
||||
->selectRaw("$haversine AS distance", [$latitude, $longitude, $latitude])
|
||||
->where('latitude', '!=', 0)
|
||||
->where('longitude', '!=', 0)
|
||||
// CHANGE THIS: Use whereRaw instead of having
|
||||
->whereRaw("$haversine < ?", [$latitude, $longitude, $latitude, $searchRadius])
|
||||
->orderBy('distance', 'asc');
|
||||
|
||||
$nearbyQuery = $applyAuthFilters($nearbyQuery);
|
||||
$nearbyItemExists = $nearbyQuery->exists();
|
||||
|
||||
if ($nearbyItemExists) {
|
||||
$query = $nearbyQuery;
|
||||
if (!$locationMessage) {
|
||||
$locationMessage = __('No Ads found at your location. Showing nearby Ads.');
|
||||
}
|
||||
} else {
|
||||
if ($isHomePage) {
|
||||
$query = clone $baseQueryBeforeLocation;
|
||||
if (!$locationMessage) {
|
||||
$locationMessage = __('No Ads found at your location. Showing all available Ads.');
|
||||
}
|
||||
} else {
|
||||
$query = $nearbyQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['query' => $query, 'message' => $locationMessage];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get watermark configuration status (enabled/disabled)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function getWatermarkConfigStatus(): bool
|
||||
{
|
||||
$enabled = CachingService::getSystemSettings('watermark_enabled');
|
||||
return !empty($enabled) && (int)$enabled === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get watermark configuration as decoded array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getWatermarkConfigDecoded(): array
|
||||
{
|
||||
$settings = CachingService::getSystemSettings([
|
||||
'watermark_enabled',
|
||||
'watermark_image',
|
||||
'watermark_opacity',
|
||||
'watermark_size',
|
||||
'watermark_style',
|
||||
'watermark_position',
|
||||
'watermark_rotation',
|
||||
]);
|
||||
|
||||
return [
|
||||
'enabled' => (int)($settings['watermark_enabled'] ?? 0),
|
||||
'watermark_image' => $settings['watermark_image'] ?? null,
|
||||
'opacity' => (int)($settings['watermark_opacity'] ?? 25),
|
||||
'size' => (int)($settings['watermark_size'] ?? 10),
|
||||
'style' => $settings['watermark_style'] ?? 'tile',
|
||||
'position' => $settings['watermark_position'] ?? 'center',
|
||||
'rotation' => (int)($settings['watermark_rotation'] ?? -30),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific setting value
|
||||
*
|
||||
* @param string $key
|
||||
* @return string|null
|
||||
*/
|
||||
public static function getSettingData(string $key): ?string
|
||||
{
|
||||
return CachingService::getSystemSettings($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate item expiry date based on package listing duration
|
||||
* Priority: listing_duration > package expiry > default
|
||||
*
|
||||
* @param \App\Models\Package|null $package
|
||||
* @param \App\Models\UserPurchasedPackage|null $userPackage
|
||||
* @return \Carbon\Carbon|null
|
||||
*/
|
||||
public static function calculateItemExpiryDate($package = null, $userPackage = null)
|
||||
{
|
||||
|
||||
if (!$package) {
|
||||
$freeAdUnlimited = Setting::where('name', 'free_ad_unlimited')->value('value') ?? 0;
|
||||
$freeAdDays = Setting::where('name', 'free_ad_duration_days')->value('value') ?? 0;
|
||||
// Unlimited free ads
|
||||
if ((int) $freeAdUnlimited === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Limited free ads
|
||||
if (!empty($freeAdDays) && (int) $freeAdDays > 0) {
|
||||
return Carbon::now()->addDays((int) $freeAdDays);
|
||||
}
|
||||
|
||||
// Safety fallback (no expiry)
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// PACKAGE LOGIC (existing, unchanged)
|
||||
// ---------------------------------------
|
||||
|
||||
$listingDurationType = $package->listing_duration_type ?? null;
|
||||
$listingDurationDays = $package->listing_duration_days ?? null;
|
||||
|
||||
// If listing_duration_type is null → use package expiry
|
||||
if ($listingDurationType === null) {
|
||||
if ($userPackage && $userPackage->end_date) {
|
||||
return Carbon::parse($userPackage->end_date);
|
||||
}
|
||||
|
||||
if ($package->duration === 'unlimited') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Carbon::now()->addDays((int) $package->duration);
|
||||
}
|
||||
|
||||
if ($listingDurationType === 'package') {
|
||||
if ($package->duration === 'unlimited') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Carbon::now()->addDays((int) $package->duration);
|
||||
}
|
||||
|
||||
if ($listingDurationType === 'custom') {
|
||||
if (!empty($listingDurationDays) && (int) $listingDurationDays > 0) {
|
||||
return Carbon::now()->addDays((int) $listingDurationDays);
|
||||
}
|
||||
|
||||
return Carbon::now()->addDays(30);
|
||||
}
|
||||
|
||||
// Standard fallback
|
||||
if (!empty($listingDurationDays) && (int) $listingDurationDays > 0) {
|
||||
return Carbon::now()->addDays((int) $listingDurationDays);
|
||||
}
|
||||
|
||||
return Carbon::now()->addDays(30);
|
||||
}
|
||||
}
|
||||
267
app/Services/NotificationService.php
Normal file
267
app/Services/NotificationService.php
Normal file
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Models\UserFcmToken;
|
||||
use Google\Client;
|
||||
use Google\Exception;
|
||||
use Illuminate\Http\Request as HttpRequest;
|
||||
use RuntimeException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Throwable;
|
||||
|
||||
class NotificationService {
|
||||
/**
|
||||
* @param array $registrationIDs
|
||||
* @param string|null $title
|
||||
* @param string|null $message
|
||||
* @param string $type
|
||||
* @param array $customBodyFields
|
||||
* @return string|array|bool
|
||||
*/
|
||||
public static function sendFcmNotification(
|
||||
array $registrationIDs,
|
||||
string|null $title = '',
|
||||
string|null $message = '',
|
||||
string $type = "default",
|
||||
array $customBodyFields = [],
|
||||
bool $sendToAll = false
|
||||
): string|array|bool {
|
||||
try {
|
||||
$project_id = Setting::select('value')->where('name', 'firebase_project_id')->first();
|
||||
if (empty($project_id->value)) {
|
||||
return ['error' => true, 'message' => 'FCM configurations are not configured.'];
|
||||
}
|
||||
$project_id = $project_id->value;
|
||||
$url = 'https://fcm.googleapis.com/v1/projects/' . $project_id . '/messages:send';
|
||||
|
||||
$access_token = self::getAccessToken();
|
||||
if ($access_token['error']) {
|
||||
return $access_token;
|
||||
}
|
||||
$dataWithTitle = [
|
||||
...$customBodyFields,
|
||||
"title" => $title,
|
||||
"body" => $message,
|
||||
"type" => $type,
|
||||
];
|
||||
|
||||
|
||||
|
||||
// ✅ Case 1: Send to all (topic-based)
|
||||
if ($sendToAll) {
|
||||
|
||||
$data = [
|
||||
"message" => [
|
||||
"topic" => "allUsers", // universal topic (subscribe everyone here)
|
||||
"data" => self::convertToStringRecursively($dataWithTitle),
|
||||
"notification" => [
|
||||
"title" => $title,
|
||||
"body" => $message
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$encodedData = json_encode($data);
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $access_token['data'],
|
||||
'Content-Type: application/json',
|
||||
];
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $encodedData);
|
||||
|
||||
$result = curl_exec($ch);
|
||||
if (!$result) {
|
||||
return ['error' => true, 'message' => 'Curl failed: ' . curl_error($ch)];
|
||||
}
|
||||
curl_close($ch);
|
||||
return ['error' => false, 'message' => "Bulk notification sent via topic", 'data' => $result];
|
||||
}
|
||||
|
||||
// ✅ Case 2: Send individually (like existing code)
|
||||
$deviceInfo = UserFcmToken::with('user')
|
||||
->select(['platform_type', 'fcm_token'])
|
||||
->whereIn('fcm_token', $registrationIDs)
|
||||
->whereHas('user', fn($q) => $q->where('notification', 1))
|
||||
->get();
|
||||
|
||||
// ✅ Log all targeted users and their FCM tokens before sending
|
||||
try {
|
||||
// Log::info('🔔 Device Info for Notification', $deviceInfo->toArray());
|
||||
} catch (Throwable $e) {
|
||||
Log::error('Failed to log device info: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
|
||||
$result = [];
|
||||
foreach ($registrationIDs as $registrationID) {
|
||||
$platform = $deviceInfo->first(fn($q) => $q->fcm_token == $registrationID);
|
||||
if (!$platform) continue;
|
||||
$data = [
|
||||
'message' => [
|
||||
'token' => $registrationID,
|
||||
|
||||
// Notification block (title & body)
|
||||
'notification' => [
|
||||
'title' => $title,
|
||||
'body' => $message,
|
||||
],
|
||||
|
||||
// Custom data (ALL values must be strings)
|
||||
'data' => self::convertToStringRecursively($dataWithTitle),
|
||||
|
||||
// Android config
|
||||
'android' => [
|
||||
'priority' => 'high',
|
||||
'notification' => [
|
||||
'image' => !empty($dataWithTitle['image']) ? $dataWithTitle['image'] : null,
|
||||
],
|
||||
],
|
||||
|
||||
// iOS (APNs) config
|
||||
'apns' => [
|
||||
'headers' => [
|
||||
'apns-priority' => '10',
|
||||
],
|
||||
'payload' => [
|
||||
'aps' => [
|
||||
'alert' => [
|
||||
'title' => $title,
|
||||
'body' => $message,
|
||||
],
|
||||
'sound' => 'default',
|
||||
'mutable-content' => 1, // REQUIRED for images
|
||||
],
|
||||
],
|
||||
'fcm_options' => [
|
||||
'image' => !empty($dataWithTitle['image']) ? $dataWithTitle['image'] : null,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
$encodedData = json_encode($data);
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $access_token['data'],
|
||||
'Content-Type: application/json',
|
||||
];
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $encodedData);
|
||||
|
||||
$result[] = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
return ['error' => false, 'message' => "Individual notifications sent", 'data' => $result];
|
||||
} catch (Throwable $th) {
|
||||
throw new RuntimeException($th);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getAccessToken() {
|
||||
try {
|
||||
$file_name = Setting::select('value')->where('name', 'service_file')->first();
|
||||
if (empty($file_name)) {
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => 'FCM Configuration not found'
|
||||
];
|
||||
}
|
||||
$disk = config('filesystems.default');
|
||||
$file = $file_name->value;
|
||||
|
||||
if ($disk === 'local' || $disk === 'public') {
|
||||
// LOCAL STORAGE
|
||||
$file_path = Storage::disk($disk)->path($file);
|
||||
|
||||
} else {
|
||||
// S3 (or any cloud disk)
|
||||
// Download file to local temp
|
||||
$fileContent = Storage::disk($disk)->get($file);
|
||||
$file_path = storage_path('app/firebase_service.json');
|
||||
file_put_contents($file_path, $fileContent);
|
||||
}
|
||||
|
||||
if (!file_exists($file_path)) {
|
||||
return [
|
||||
'error' => true,
|
||||
'message' => 'FCM Service File not found'
|
||||
];
|
||||
}
|
||||
$client = new Client();
|
||||
$client->setAuthConfig($file_path);
|
||||
$client->setScopes(['https://www.googleapis.com/auth/firebase.messaging']);
|
||||
|
||||
return [
|
||||
'error' => false,
|
||||
'message' => 'Access Token generated successfully',
|
||||
'data' => $client->fetchAccessTokenWithAssertion()['access_token']
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
throw new RuntimeException($e);
|
||||
}
|
||||
}
|
||||
|
||||
public static function convertToStringRecursively($data, &$flattenedArray = []) {
|
||||
foreach ($data as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
self::convertToStringRecursively($value, $flattenedArray);
|
||||
} elseif (is_null($value)) {
|
||||
$flattenedArray[$key] = '';
|
||||
} else {
|
||||
$flattenedArray[$key] = (string)$value;
|
||||
}
|
||||
}
|
||||
return $flattenedArray;
|
||||
}
|
||||
public static function sendNewDeviceLoginEmail(User $user, HttpRequest $request)
|
||||
{
|
||||
try {
|
||||
Log::info("sending New device login alert");
|
||||
|
||||
$deviceType = ucfirst($request->platform_type ?? 'Unknown');
|
||||
$ip = request()->ip();
|
||||
$loginTime = now()->format('d M Y - h:i A');
|
||||
|
||||
// Fetch company name
|
||||
$companyName = Setting::where('name', 'company_name')->value('value') ?? 'Unknown';
|
||||
|
||||
// Email message
|
||||
$message =
|
||||
"A new device has just logged in to your {$companyName} account.\n\n" .
|
||||
"⏰ Login Time: {$loginTime}\n\n";
|
||||
|
||||
Mail::raw($message, function ($msg) use ($user, $companyName) {
|
||||
$msg->to($user->email)
|
||||
->from('admin@yourdomain.com', $companyName)
|
||||
->subject("New Device Login Detected - {$companyName}");
|
||||
});
|
||||
|
||||
Log::info("New device login alert sent to: {$user->email}");
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to send new device login email: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
141
app/Services/Payment/FlutterWavePayment.php
Normal file
141
app/Services/Payment/FlutterWavePayment.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payment;
|
||||
|
||||
use App\Services\ResponseService;
|
||||
use KingFlamez\Rave\Rave as Flutterwave;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
|
||||
|
||||
class FlutterwavePayment implements PaymentInterface
|
||||
{
|
||||
private string $currencyCode;
|
||||
private Flutterwave $flutterwave;
|
||||
private string $encryptionKey;
|
||||
protected $baseUrl;
|
||||
private string $secretKey;
|
||||
|
||||
public function __construct($secret_key, $public_key, $encryption_key, $currencyCode)
|
||||
{
|
||||
$this->currencyCode = $currencyCode;
|
||||
$this->encryptionKey = $encryption_key;
|
||||
$this->baseUrl = 'https://api.flutterwave.com/v3';
|
||||
$this->secretKey = $secret_key;
|
||||
// Initialize Flutterwave SDK with API keys
|
||||
$this->flutterwave = new Flutterwave([
|
||||
'publicKey' => $public_key,
|
||||
'secretKey' => $secret_key,
|
||||
'encryptionKey' => $encryption_key,
|
||||
]);
|
||||
}
|
||||
|
||||
public function createPaymentIntent($amount, $customMetaData)
|
||||
{
|
||||
try {
|
||||
if (empty($customMetaData['email'])) {
|
||||
throw new Exception("Email cannot be empty");
|
||||
}
|
||||
$redirectUrl = ($customMetaData['platform_type'] == 'app')
|
||||
? route('flutterwave.success')
|
||||
: route('flutterwave.success.web');
|
||||
|
||||
$finalAmount =$amount;
|
||||
// $transactionRef = uniqid('flw_');
|
||||
$transactionRef = 't' .'-'. $customMetaData['payment_transaction_id'] .'-'. 'p' .'-'. $customMetaData['package_id'];
|
||||
$data = [
|
||||
'tx_ref' => $transactionRef,
|
||||
'amount' => $finalAmount,
|
||||
'currency' => $this->currencyCode,
|
||||
'redirect_url' => $redirectUrl,
|
||||
'payment_options' => 'card,banktransfer', // You can add more payment options
|
||||
'customer' => [
|
||||
'email' => $customMetaData['email'],
|
||||
'phonenumber' => $customMetaData['phone'] ?? Auth::user()->mobile,
|
||||
'name' => $customMetaData['name'] ?? Auth::user()->name,
|
||||
],
|
||||
'meta' => [
|
||||
'package_id' => $customMetaData['package_id'],
|
||||
'user_id' => $customMetaData['user_id'],
|
||||
]
|
||||
];
|
||||
$data = json_encode($data, JSON_UNESCAPED_SLASHES);
|
||||
$url = 'https://api.flutterwave.com/v3/payments';
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $this->secretKey
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
if (curl_errno($ch)) {
|
||||
return ['error' => curl_error($ch)];
|
||||
}
|
||||
curl_close($ch);
|
||||
$payment = json_decode($response,true);
|
||||
return $this->formatPaymentIntent($transactionRef, $finalAmount,$this->currencyCode,'pending',$customMetaData,$payment);
|
||||
} catch (Exception $e) {
|
||||
return ResponseService::errorResponse("Payment failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function createAndFormatPaymentIntent($amount, $customMetaData): array
|
||||
{
|
||||
return $this->createPaymentIntent($amount, $customMetaData);
|
||||
}
|
||||
|
||||
public function retrievePaymentIntent($transactionId): array
|
||||
{
|
||||
try {
|
||||
$response = $this->flutterwave->verifyTransaction($transactionId);
|
||||
if ($response['status'] === 'success') {
|
||||
return $this->formatPaymentIntent(
|
||||
$response['data']['tx_ref'],
|
||||
$response['data']['amount'],
|
||||
$response['data']['currency'],
|
||||
$response['data']['status'],
|
||||
[],
|
||||
$response
|
||||
);
|
||||
}
|
||||
throw new Exception("Error fetching payment status: " . $response['message']);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception("Error verifying transaction: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function formatPaymentIntent($id, $amount, $currency, $status, $metadata, $paymentIntent): array
|
||||
{
|
||||
return [
|
||||
'id' => $id,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'metadata' => $metadata,
|
||||
'status' => match ($status) {
|
||||
'successful' => 'succeeded',
|
||||
'pending' => 'pending',
|
||||
'failed' => 'failed',
|
||||
default => 'unknown'
|
||||
},
|
||||
'payment_gateway_response' => $paymentIntent
|
||||
];
|
||||
}
|
||||
|
||||
public function minimumAmountValidation($currency, $amount)
|
||||
{
|
||||
$minimumAmount = match ($currency) {
|
||||
'NGN' => 50, // 50 Naira
|
||||
default => 1.00
|
||||
};
|
||||
|
||||
return ($amount >= $minimumAmount) ? $amount : $minimumAmount;
|
||||
}
|
||||
}
|
||||
251
app/Services/Payment/PayPalPayment.php
Normal file
251
app/Services/Payment/PayPalPayment.php
Normal file
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payment;
|
||||
|
||||
use Exception;
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
class PayPalPayment implements PaymentInterface
|
||||
{
|
||||
private string $clientId;
|
||||
private string $secretKey;
|
||||
private string $currencyCode;
|
||||
private string $paymentmode;
|
||||
private Client $http;
|
||||
private string $baseUrl;
|
||||
|
||||
|
||||
public function __construct($clientId, $secretKey, $currencyCode, $paymentmode)
|
||||
{
|
||||
$this->clientId = $clientId;
|
||||
$this->secretKey = $secretKey;
|
||||
$this->currencyCode = $currencyCode;
|
||||
$this->paymentmode = $paymentmode;
|
||||
|
||||
$this->http = new Client([
|
||||
'base_uri' => ($paymentmode == "UAT") ? 'https://api.sandbox.paypal.com' : 'https://api-m.paypal.com',
|
||||
'timeout' => 30,
|
||||
]);
|
||||
$this->baseUrl = ($paymentmode == "UAT")
|
||||
? 'https://api.sandbox.paypal.com'
|
||||
: 'https://api.paypal.com';
|
||||
|
||||
}
|
||||
|
||||
private function generateAccessToken(): string
|
||||
{
|
||||
$response = $this->http->post('/v1/oauth2/token', [
|
||||
'auth' => [$this->clientId, $this->secretKey],
|
||||
'form_params' => ['grant_type' => 'client_credentials'],
|
||||
'headers' => [
|
||||
'Accept' => 'application/json',
|
||||
'Accept-Language' => 'en_US'
|
||||
]
|
||||
]);
|
||||
|
||||
$data = json_decode((string)$response->getBody(), true);
|
||||
return $data['access_token'] ?? throw new Exception("Unable to generate PayPal token");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PayPal Order (Payment Intent equivalent)
|
||||
*/
|
||||
public function createPaymentIntent($amount, $customMetaData)
|
||||
{
|
||||
$amount = $this->minimumAmountValidation($this->currencyCode, $amount);
|
||||
$accessToken = $this->generateAccessToken();
|
||||
if($customMetaData['platform_type'] == 'app') {
|
||||
$callbackUrl = route('paypal.success') ;
|
||||
}else{
|
||||
$callbackUrl = route('paypal.success.web');
|
||||
}
|
||||
$metaData = 't' . '-' . $customMetaData['payment_transaction_id'] . '-' . 'p' . '-' . $customMetaData['package_id'];
|
||||
$response = $this->http->post('/v2/checkout/orders', [
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$accessToken}",
|
||||
'Content-Type' => 'application/json'
|
||||
],
|
||||
'json' => [
|
||||
'intent' => 'CAPTURE',
|
||||
'purchase_units' => [[
|
||||
'amount' => [
|
||||
'currency_code' => $this->currencyCode,
|
||||
'value' => number_format($amount, 2, '.', '')
|
||||
],
|
||||
'custom_id' => $metaData,
|
||||
'description' => $customMetaData['description'] ?? null,
|
||||
]],
|
||||
'application_context' => [
|
||||
'return_url' => $callbackUrl,
|
||||
'cancel_url' => $callbackUrl,
|
||||
]
|
||||
]
|
||||
|
||||
]);
|
||||
|
||||
$data = json_decode((string)$response->getBody(), true);
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ✅ Matches PaymentInterface (like Stripe)
|
||||
*/
|
||||
public function createAndFormatPaymentIntent($amount, $customMetaData): array
|
||||
{
|
||||
$order = $this->createPaymentIntent($amount, $customMetaData);
|
||||
|
||||
// Extract approve link from PayPal response
|
||||
$approveLink = null;
|
||||
if (isset($order['links']) && is_array($order['links'])) {
|
||||
foreach ($order['links'] as $link) {
|
||||
if ($link['rel'] === 'approve') {
|
||||
$approveLink = $link['href'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$formatted = $this->formatPaymentIntent(
|
||||
$order['id'],
|
||||
$amount,
|
||||
$this->currencyCode,
|
||||
$order['status'] ?? 'CREATED',
|
||||
$customMetaData,
|
||||
$order
|
||||
);
|
||||
|
||||
// Add approval link for frontend
|
||||
$formatted['approval_url'] = $approveLink;
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ✅ Retrieve order details (similar to Stripe’s retrievePaymentIntent)
|
||||
*/
|
||||
public function retrievePaymentIntent($paymentId): array
|
||||
{
|
||||
$accessToken = $this->generateAccessToken();
|
||||
|
||||
$response = $this->http->get("/v2/checkout/orders/{$paymentId}", [
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$accessToken}",
|
||||
'Content-Type' => 'application/json'
|
||||
]
|
||||
]);
|
||||
|
||||
$data = json_decode((string)$response->getBody(), true);
|
||||
|
||||
return $this->formatPaymentIntent(
|
||||
$data['id'],
|
||||
$data['purchase_units'][0]['amount']['value'] ?? 0,
|
||||
$data['purchase_units'][0]['amount']['currency_code'] ?? $this->currencyCode,
|
||||
$data['status'],
|
||||
[],
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
public function capturePayment($orderId): array
|
||||
{
|
||||
$accessToken = $this->generateAccessToken();
|
||||
|
||||
$response = $this->http->post("/v2/checkout/orders/{$orderId}/capture", [
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$accessToken}",
|
||||
'Content-Type' => 'application/json'
|
||||
]
|
||||
]);
|
||||
|
||||
$data = json_decode((string)$response->getBody(), true);
|
||||
|
||||
return $this->formatPaymentIntent(
|
||||
$data['id'] ?? $orderId,
|
||||
$data['purchase_units'][0]['payments']['captures'][0]['amount']['value'] ?? 0,
|
||||
$data['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code'] ?? $this->currencyCode,
|
||||
$data['status'] ?? 'FAILED',
|
||||
[],
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
public function refund($captureId, $amount, $currency = null): array
|
||||
{
|
||||
$accessToken = $this->generateAccessToken();
|
||||
$currency = $currency ?? $this->currencyCode;
|
||||
|
||||
$response = $this->http->post("/v2/payments/captures/{$captureId}/refund", [
|
||||
'headers' => [
|
||||
'Authorization' => "Bearer {$accessToken}",
|
||||
'Content-Type' => 'application/json'
|
||||
],
|
||||
'json' => [
|
||||
'amount' => [
|
||||
'value' => number_format($amount, 2, '.', ''),
|
||||
'currency_code' => $currency
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
return json_decode((string)$response->getBody(), true);
|
||||
}
|
||||
|
||||
public function formatPaymentIntent($id, $amount, $currency, $status, $metadata, $paymentIntent): array
|
||||
{
|
||||
return [
|
||||
'id' => $id,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'metadata' => $metadata,
|
||||
'status' => match (strtolower($status)) {
|
||||
"completed" => "succeed",
|
||||
"approved" => "pending",
|
||||
"created" => "pending",
|
||||
default => "failed",
|
||||
},
|
||||
'payment_gateway_response' => $paymentIntent
|
||||
];
|
||||
}
|
||||
|
||||
public function minimumAmountValidation($currency, $amount)
|
||||
{
|
||||
$minimumAmount = 0.50;
|
||||
return max($amount, $minimumAmount);
|
||||
}
|
||||
|
||||
public function verifyWebhookSignature(array $headers, string $payload, string $webhookId): bool
|
||||
{
|
||||
try {
|
||||
$accessToken = $this->generateAccessToken();
|
||||
if($this->baseUrl == 'https://api.sandbox.paypal.com'){
|
||||
return true;
|
||||
}
|
||||
$verification = Http::withToken($accessToken)
|
||||
->post($this->baseUrl . '/v1/notifications/verify-webhook-signature', [
|
||||
'auth_algo' => $headers['paypal-auth-algo'][0] ?? '',
|
||||
'cert_url' => $headers['paypal-cert-url'][0] ?? '',
|
||||
'transmission_id' => $headers['paypal-transmission-id'][0] ?? '',
|
||||
'transmission_sig' => $headers['paypal-transmission-sig'][0] ?? '',
|
||||
'transmission_time' => $headers['paypal-transmission-time'][0] ?? '',
|
||||
'webhook_id' => $webhookId,
|
||||
'webhook_event' => json_decode($payload, true),
|
||||
]);
|
||||
|
||||
if (!$verification->successful()) {
|
||||
Log::error('PayPal verifyWebhookSignature failed: ' . $verification->body());
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($verification->json()['verification_status'] ?? '') === 'SUCCESS';
|
||||
} catch (Throwable $e) {
|
||||
Log::error('PayPal verifyWebhookSignature exception: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
17
app/Services/Payment/PaymentInterface.php
Normal file
17
app/Services/Payment/PaymentInterface.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payment;
|
||||
|
||||
interface PaymentInterface {
|
||||
public function createPaymentIntent($amount, $customMetaData);
|
||||
|
||||
public function createAndFormatPaymentIntent($amount, $customMetaData): array;
|
||||
|
||||
public function retrievePaymentIntent($paymentId): array;
|
||||
|
||||
public function minimumAmountValidation($currency, $amount);
|
||||
|
||||
public function formatPaymentIntent($id, $amount, $currency, $status, $metadata, $paymentIntent): array;
|
||||
//
|
||||
// public function checkPayment(Order $order): PaymentStatus;
|
||||
}
|
||||
71
app/Services/Payment/PaymentService.php
Normal file
71
app/Services/Payment/PaymentService.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payment;
|
||||
|
||||
use App\Models\PaymentConfiguration;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class PaymentService {
|
||||
/**
|
||||
* @param string $paymentGateway - Stripe
|
||||
* @return StripePayment
|
||||
*/
|
||||
public static function create(string $paymentGateway) {
|
||||
$paymentGateway = strtolower($paymentGateway);
|
||||
$payment = PaymentConfiguration::where(['payment_method' => $paymentGateway, 'status' => 1])->first();
|
||||
|
||||
if (!$payment) {
|
||||
throw new InvalidArgumentException('Invalid Payment Gateway.');
|
||||
}
|
||||
return match ($paymentGateway) {
|
||||
'stripe' => new StripePayment($payment->secret_key, $payment->currency_code),
|
||||
'paystack' => new PaystackPayment($payment->currency_code),
|
||||
'razorpay' => new RazorpayPayment($payment->secret_key, $payment->api_key, $payment->currency_code),
|
||||
'phonepe' => new PhonePePayment($payment->secret_key , $payment->api_key,$payment->additional_data_1,$payment->additional_data_2,$payment->payment_mode),
|
||||
'flutterwave' => new FlutterWavePayment($payment->secret_key, $payment->api_key, $payment->webhook_secret_key, $payment->currency_code),
|
||||
'paypal' => new PayPalPayment($payment->api_key, $payment->secret_key, $payment->currency_code,$payment->payment_mode),
|
||||
'google,apple' => null,
|
||||
// any other payment processor implementations
|
||||
default => throw new InvalidArgumentException('Invalid Payment Gateway.'),
|
||||
};
|
||||
}
|
||||
|
||||
/***
|
||||
* @param string $paymentGateway
|
||||
* @param $paymentIntentData
|
||||
* @return array
|
||||
* Stripe Payment Intent : https://stripe.com/docs/api/payment_intents/object
|
||||
*/
|
||||
// public static function formatPaymentIntent(string $paymentGateway, $paymentIntentData) {
|
||||
// $paymentGateway = strtolower($paymentGateway);
|
||||
// return match ($paymentGateway) {
|
||||
// 'stripe' => [
|
||||
// 'id' => $paymentIntentData->id,
|
||||
// 'amount' => $paymentIntentData->amount,
|
||||
// 'currency' => $paymentIntentData->currency,
|
||||
// 'metadata' => $paymentIntentData->metadata,
|
||||
// 'status' => match ($paymentIntentData->status) {
|
||||
// "canceled" => "failed",
|
||||
// "succeeded" => "succeed",
|
||||
// "processing", "requires_action", "requires_capture", "requires_confirmation", "requires_payment_method" => "pending",
|
||||
// },
|
||||
// 'payment_gateway_response' => $paymentIntentData
|
||||
// ],
|
||||
//
|
||||
// 'paystack' => [
|
||||
// 'id' => $paymentIntentData['data']['reference'],
|
||||
// 'amount' => $paymentIntentData->amount,
|
||||
// 'currency' => $paymentIntentData->currency,
|
||||
// 'metadata' => $paymentIntentData->metadata,
|
||||
// 'status' => match ($paymentIntentData['data']['status']) {
|
||||
// "abandoned" => "failed",
|
||||
// "succeed" => "succeed",
|
||||
// default => $paymentIntentData['data']['status'] ?? true
|
||||
// },
|
||||
// 'payment_gateway_response' => $paymentIntentData
|
||||
// ],
|
||||
// // any other payment processor implementations
|
||||
// default => $paymentIntentData,
|
||||
// };
|
||||
// }
|
||||
}
|
||||
132
app/Services/Payment/PaystackPayment.php
Normal file
132
app/Services/Payment/PaystackPayment.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payment;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
use Unicodeveloper\Paystack\Paystack;
|
||||
|
||||
class PaystackPayment extends Paystack implements PaymentInterface {
|
||||
private Paystack $paystack;
|
||||
private string $currencyCode;
|
||||
|
||||
/**
|
||||
* PaystackPayment constructor.
|
||||
* @param $currencyCode
|
||||
*/
|
||||
public function __construct($currencyCode) {
|
||||
// Call Paystack Class and Create Payment Intent
|
||||
$this->paystack = new Paystack();
|
||||
$this->currencyCode = $currencyCode;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $amount
|
||||
* @param $customMetaData
|
||||
* @return array
|
||||
*/
|
||||
public function createPaymentIntent($amount, $customMetaData) {
|
||||
|
||||
try {
|
||||
|
||||
if (empty($customMetaData['email'])) {
|
||||
throw new RuntimeException("Email cannot be empty");
|
||||
}
|
||||
if($customMetaData['platform_type'] == 'app') {
|
||||
$callbackUrl = route('paystack.success') ;
|
||||
}else{
|
||||
$callbackUrl = route('paystack.success.web');
|
||||
}
|
||||
|
||||
$finalAmount = $amount * 100;
|
||||
$reference = $this->genTranxRef();
|
||||
|
||||
|
||||
$data = [
|
||||
'amount' => $finalAmount,
|
||||
'currency' => $this->currencyCode,
|
||||
'email' => $customMetaData['email'],
|
||||
'metadata' => $customMetaData,
|
||||
'reference' => $reference,
|
||||
'callback_url' => $callbackUrl
|
||||
];
|
||||
|
||||
return $this->paystack->getAuthorizationResponse($data);
|
||||
|
||||
} catch (Throwable $e) {
|
||||
throw new RuntimeException($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $amount
|
||||
* @param $customMetaData
|
||||
* @return array
|
||||
*/
|
||||
public function createAndFormatPaymentIntent($amount, $customMetaData): array {
|
||||
$response = $this->createPaymentIntent($amount, $customMetaData);
|
||||
return $this->format($response, $amount, $this->currencyCode, $customMetaData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $paymentId
|
||||
* @return array
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function retrievePaymentIntent($paymentId): array {
|
||||
try {
|
||||
$relativeUrl = "/transaction/verify/{$paymentId}";
|
||||
$this->response = $this->client->get($this->baseUrl . $relativeUrl, []);
|
||||
$response = json_decode($this->response->getBody(), true, 512, JSON_THROW_ON_ERROR);
|
||||
return $this->format($response['data'], $response['data']['amount'], $response['data']['currency'], $response['data']['metadata']);
|
||||
} catch (Throwable $e) {
|
||||
throw new RuntimeException($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $currency
|
||||
* @param $amount
|
||||
*/
|
||||
public function minimumAmountValidation($currency, $amount) {
|
||||
// TODO: Implement minimumAmountValidation() method.
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $paymentIntent
|
||||
* @param $amount
|
||||
* @param $currencyCode
|
||||
* @param $metadata
|
||||
* @return array
|
||||
*/
|
||||
public function format($paymentIntent, $amount, $currencyCode, $metadata) {
|
||||
return $this->formatPaymentIntent($paymentIntent['data']['reference'], $amount, $currencyCode, $paymentIntent['status'], $metadata, $paymentIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @param $amount
|
||||
* @param $currency
|
||||
* @param $status
|
||||
* @param $metadata
|
||||
* @param $paymentIntent
|
||||
* @return array
|
||||
*/
|
||||
public function formatPaymentIntent($id, $amount, $currency, $status, $metadata, $paymentIntent): array {
|
||||
return [
|
||||
'id' => $id,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'metadata' => $metadata,
|
||||
'status' => match ($status) {
|
||||
"abandoned" => "failed",
|
||||
"succeed" => "succeed",
|
||||
default => $status ?? true
|
||||
},
|
||||
'payment_gateway_response' => $paymentIntent
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
382
app/Services/Payment/PhonePePayment.php
Normal file
382
app/Services/Payment/PhonePePayment.php
Normal file
@@ -0,0 +1,382 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payment;
|
||||
|
||||
use Auth;
|
||||
use PhonePe\PhonePe as PhonePeSDK;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Exception;
|
||||
|
||||
class PhonePePayment implements PaymentInterface
|
||||
{
|
||||
private string $clientId;
|
||||
private string $callbackUrl;
|
||||
private string $transactionId;
|
||||
private string $clientSecret;
|
||||
private string $clientVersion;
|
||||
private string $payment_mode;
|
||||
private string $merchantId;
|
||||
private string $pgUrl;
|
||||
|
||||
public function __construct($clientSecret, $clientId, $addtional_data_1,$addtional_data_2, $payment_mode)
|
||||
{
|
||||
// $this->clientId = 'TEST-CITYSURFONLINE_2508';
|
||||
$this->clientId = $clientId;
|
||||
$this->callbackUrl = url('/webhook/phonePe');
|
||||
$this->transactionId = uniqid();
|
||||
$this->clientSecret = $clientSecret;
|
||||
$this->clientVersion = $addtional_data_1;
|
||||
$this->payment_mode = $payment_mode;
|
||||
$this->merchantId = $addtional_data_2;
|
||||
$this->pgUrl = ($payment_mode == "UAT") ? "https://api-preprod.phonepe.com/apis/pg-sandbox" : "https://api.phonepe.com/apis/pg";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create payment intent for PhonePe
|
||||
*
|
||||
* @param $amount
|
||||
* @param $customMetaData
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public function createPaymentIntent($amount, $customMetaData) {
|
||||
Log::info("PhonePe Payment custome", [
|
||||
'amount' => $amount,
|
||||
'customMetaData' => $customMetaData,
|
||||
]);
|
||||
$amount = $this->minimumAmountValidation('INR', $amount);
|
||||
$userMobile = Auth::user()->mobile;
|
||||
$metaData = 't' . '-' . $customMetaData['payment_transaction_id'] . '-' . 'p' . '-' . $customMetaData['package_id'];
|
||||
|
||||
if ($customMetaData['platform_type'] == 'web') {
|
||||
$transactionId = uniqid();
|
||||
$mode = $this->payment_mode;
|
||||
if ($mode === 'PROD') {
|
||||
$tokenUrl = 'https://api.phonepe.com/apis/identity-manager/v1/oauth/token';
|
||||
$orderUrl = 'https://api.phonepe.com/apis/pg/checkout/v2/sdk/order';
|
||||
$payUrl = 'https://api.phonepe.com/apis/pg/checkout/v2/pay';
|
||||
} else {
|
||||
$tokenUrl = 'https://api-preprod.phonepe.com/apis/pg-sandbox/v1/oauth/token';
|
||||
$orderUrl = 'https://api-preprod.phonepe.com/apis/pg-sandbox/checkout/v2/sdk/order';
|
||||
$payUrl = 'https://api-preprod.phonepe.com/apis/pg-sandbox/checkout/v2/pay';
|
||||
}
|
||||
$tokenCurl = curl_init();
|
||||
|
||||
curl_setopt_array($tokenCurl, array(
|
||||
CURLOPT_URL => $tokenUrl,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_MAXREDIRS => 10,
|
||||
CURLOPT_TIMEOUT => 0,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
||||
CURLOPT_CUSTOMREQUEST => 'POST',
|
||||
CURLOPT_POSTFIELDS => http_build_query([
|
||||
'client_id' => $this->clientId,
|
||||
'client_version' => $this->clientVersion,
|
||||
'client_secret' => $this->clientSecret,
|
||||
'grant_type' => 'client_credentials',
|
||||
]),
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Content-Type: application/x-www-form-urlencoded'
|
||||
),
|
||||
));
|
||||
|
||||
$response = curl_exec($tokenCurl);
|
||||
curl_close($tokenCurl);
|
||||
|
||||
// Decode token response
|
||||
$tokenResponse = json_decode($response, true);
|
||||
$accessToken = $tokenResponse['access_token'] ?? null;
|
||||
|
||||
if (!$accessToken) {
|
||||
dd("Failed to retrieve token", $response);
|
||||
}
|
||||
// Build JSON payload properly
|
||||
$paymentData = [
|
||||
'merchantOrderId' => $metaData,
|
||||
'amount' => (int) round($amount * 100),
|
||||
"metadata" => [
|
||||
"package_id" => $customMetaData['package_id'],
|
||||
"payment_transaction_id" => $customMetaData['payment_transaction_id'],
|
||||
"user_id" => Auth::user()->id,
|
||||
],
|
||||
'paymentFlow' => [
|
||||
'type' => 'PG_CHECKOUT',
|
||||
'message' => 'Payment message used for collect requests',
|
||||
'merchantUrls' => [
|
||||
'redirectUrl' => route('phonepe.success.web'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$payCUrl = curl_init();
|
||||
|
||||
curl_setopt_array($payCUrl, array(
|
||||
CURLOPT_URL => $payUrl,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_MAXREDIRS => 10,
|
||||
CURLOPT_TIMEOUT => 0,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
||||
CURLOPT_CUSTOMREQUEST => 'POST',
|
||||
CURLOPT_POSTFIELDS => json_encode($paymentData),
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Content-Type: application/json',
|
||||
'Authorization: O-Bearer ' . $accessToken,
|
||||
),
|
||||
));
|
||||
|
||||
$response = curl_exec($payCUrl);
|
||||
$final_response = json_decode($response, true);
|
||||
if (!empty($final_response)) {
|
||||
|
||||
$redirectURL = $final_response['redirectUrl'];
|
||||
return $this->formatPaymentIntent($transactionId, $amount, 'INR', 'pending', $customMetaData, $redirectURL);
|
||||
}
|
||||
curl_close($payCUrl);
|
||||
} else {
|
||||
$redirectUrl = route('phonepe.success');
|
||||
$orderId = 'TX' . time(); // unique order ID
|
||||
$amount = (int) round($amount * 100); // amount in INR (not multiplied)
|
||||
$expireAfter = 1200; // in seconds (20 mins)
|
||||
$token = $this->getPhonePeToken();
|
||||
$order = $this->createOrder($token, $orderId, $amount);
|
||||
$order_data = json_decode($order, true);
|
||||
$requestPayload = [
|
||||
"orderId" => $order_data['orderId'],
|
||||
// "state" => "PENDING",
|
||||
'merchantOrderId' => $metaData,
|
||||
"merchantId" => $this->merchantId,
|
||||
"expireAT" => $expireAfter,
|
||||
"token" => $order_data['token'],
|
||||
"paymentMode" => [
|
||||
"type" => "PAY_PAGE"
|
||||
]
|
||||
];
|
||||
|
||||
// Convert to JSON string as required by Flutter SDK
|
||||
$requestString = json_encode($requestPayload);
|
||||
|
||||
if ($this->payment_mode == "UAT") {
|
||||
$payment_mode = "SANDBOX";
|
||||
} else {
|
||||
$payment_mode = "PRODUCTION";
|
||||
}
|
||||
|
||||
return [
|
||||
"environment" => $payment_mode, // or "PRODUCTION"
|
||||
"merchantId" => $this->merchantId,
|
||||
"flowId" => $orderId,
|
||||
"enableLogging" => true, // false in production
|
||||
"request" => $requestPayload,
|
||||
"appSchema" => "eclassify", // for iOS deep link return
|
||||
"token" => $token,
|
||||
];
|
||||
}
|
||||
throw new Exception("Error initiating payment: " . $redirectURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and format payment intent for PhonePe
|
||||
*
|
||||
* @param $amount
|
||||
* @param $customMetaData
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public function createAndFormatPaymentIntent($amount, $customMetaData): array
|
||||
{
|
||||
$paymentIntent = $this->createPaymentIntent($amount, $customMetaData);
|
||||
$metaData = 't' . '-' . $customMetaData['payment_transaction_id'] . '-' . 'p' . '-' . $customMetaData['package_id'];
|
||||
return $this->formatPaymentIntent(
|
||||
id: $metaData,
|
||||
amount: $amount,
|
||||
currency: 'INR',
|
||||
status: "PENDING",
|
||||
metadata: $customMetaData,
|
||||
paymentIntent: $paymentIntent
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payment intent (check payment status)
|
||||
*
|
||||
* @param $transactionId
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public function retrievePaymentIntent($transactionId): array
|
||||
{
|
||||
$statusUrl = 'https://api.phonepe.com/v3/transaction/' . $transactionId . '/status';
|
||||
$signature = $this->generateSignature(''); // Adjust if needed based on PhonePe requirements
|
||||
|
||||
$response = $this->sendRequest($statusUrl, '', $signature);
|
||||
|
||||
if ($response['success']) {
|
||||
return $this->formatPaymentIntent($transactionId, $response['amount'], 'INR', $response['status'], [], $response);
|
||||
}
|
||||
|
||||
throw new Exception("Error fetching payment status: " . $response['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format payment intent response
|
||||
*
|
||||
* @param $id
|
||||
* @param $amount
|
||||
* @param $currency
|
||||
* @param $status
|
||||
* @param $metadata
|
||||
* @param $paymentIntent
|
||||
* @return array
|
||||
*/
|
||||
public function formatPaymentIntent($id, $amount, $currency, $status, $metadata, $paymentIntent): array
|
||||
{
|
||||
return [
|
||||
'id' => $id,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'metadata' => $metadata,
|
||||
'status' => match ($status) {
|
||||
"SUCCESS" => "succeeded",
|
||||
"PENDING" => "pending",
|
||||
"FAILED" => "failed",
|
||||
default => "unknown"
|
||||
},
|
||||
'payment_gateway_response' => $paymentIntent
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum amount validation
|
||||
*
|
||||
* @param $currency
|
||||
* @param $amount
|
||||
* @return float|int
|
||||
*/
|
||||
public function minimumAmountValidation($currency, $amount)
|
||||
{
|
||||
$minimumAmount = match ($currency) {
|
||||
'INR' => 1.00, // 1 Rupee
|
||||
default => 0.50
|
||||
};
|
||||
|
||||
return ($amount >= $minimumAmount) ? $amount : $minimumAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HMAC signature for PhonePe
|
||||
*
|
||||
* @param $encodedRequestBody
|
||||
* @return string
|
||||
*/
|
||||
private function generateSignature($requestBody): string
|
||||
{
|
||||
// Concatenate raw JSON payload, endpoint, and salt key
|
||||
$stringToHash = $requestBody . '/pg/v1/pay' . $this->saltKey;
|
||||
|
||||
// Hash the string using SHA256
|
||||
$hash = hash('sha256', $stringToHash);
|
||||
|
||||
// Append salt index (Assumed to be 1 in this example)
|
||||
return $hash . '###' . 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send cURL request to PhonePe API
|
||||
*
|
||||
* @param $url
|
||||
* @param $requestBody
|
||||
* @param $signature
|
||||
* @return array
|
||||
*/
|
||||
// private function sendRequest($url, $requestBody, $signature): array
|
||||
// {
|
||||
// // dd($requestBody);
|
||||
// $ch = curl_init($url);
|
||||
// curl_setopt($ch, CURLOPT_POST, 1);
|
||||
// curl_setopt($ch, CURLOPT_POSTFIELDS, $requestBody);
|
||||
// curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
// 'Content-Type: application/json',
|
||||
// 'X-VERIFY: ' . $signature,
|
||||
// ]);
|
||||
// curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
// $response = curl_exec($ch);
|
||||
// curl_close($ch);
|
||||
// return json_decode($response, true);
|
||||
// }
|
||||
|
||||
public function getPhonePeToken() {
|
||||
$clientId = $this->clientId;
|
||||
$clientSecret = $this->clientSecret;
|
||||
$clientVersion = $this->clientVersion;
|
||||
|
||||
$postData = http_build_query([
|
||||
'client_id' => $clientId,
|
||||
'client_version' => $clientVersion,
|
||||
'client_secret' => $clientSecret,
|
||||
'grant_type' => 'client_credentials',
|
||||
]);
|
||||
|
||||
if ($this->payment_mode == "UAT") {
|
||||
$url = 'https://api-preprod.phonepe.com/apis/pg-sandbox/v1/oauth/token';
|
||||
} else {
|
||||
$url = 'https://api.phonepe.com/apis/identity-manager/v1/oauth/token';
|
||||
}
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
throw new \Exception('Curl error: ' . curl_error($ch));
|
||||
}
|
||||
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$responseData = json_decode($response, true);
|
||||
|
||||
if ($httpCode === 200 && isset($responseData['access_token'])) {
|
||||
return $responseData['access_token'];
|
||||
}
|
||||
|
||||
throw new \Exception('Failed to fetch PhonePe token. Response: ' . $response);
|
||||
}
|
||||
|
||||
public function createOrder($token, $merchantOrderId, $amount) {
|
||||
$url = $this->pgUrl . '/checkout/v2/sdk/order';
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
||||
"merchantOrderId" => $merchantOrderId,
|
||||
"amount" => $amount,
|
||||
"paymentFlow" => [
|
||||
"type" => "PG_CHECKOUT"
|
||||
]
|
||||
]));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: O-Bearer ' . $token,
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
119
app/Services/Payment/RazorpayPayment.php
Normal file
119
app/Services/Payment/RazorpayPayment.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payment;
|
||||
|
||||
use Log;
|
||||
use Razorpay\Api\Api;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class RazorpayPayment implements PaymentInterface {
|
||||
private Api $api;
|
||||
private string $currencyCode;
|
||||
|
||||
/**
|
||||
* RazorpayPayment constructor.
|
||||
* @param $secretKey
|
||||
* @param $publicKey
|
||||
* @param $currencyCode
|
||||
*/
|
||||
public function __construct($secretKey, $publicKey, $currencyCode) {
|
||||
// Call Stripe Class and Create Payment Intent
|
||||
$this->api = new Api($publicKey, $secretKey);
|
||||
$this->currencyCode = $currencyCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $amount
|
||||
* @param $customMetaData
|
||||
* @return mixed
|
||||
*/
|
||||
public function createPaymentIntent($amount, $customMetaData) {
|
||||
try {
|
||||
$orderData = [
|
||||
'amount' => $this->minimumAmountValidation($this->currencyCode, $amount),
|
||||
'currency' => $this->currencyCode,
|
||||
'notes' => $customMetaData,
|
||||
];
|
||||
return $this->api->order->create($orderData);
|
||||
} catch (Throwable $e) {
|
||||
Log::error('Failed to create payment intent: ' . $e->getMessage());
|
||||
throw new RuntimeException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $amount
|
||||
* @param $customMetaData
|
||||
* @return array
|
||||
*/
|
||||
public function createAndFormatPaymentIntent($amount, $customMetaData): array {
|
||||
$response = $this->createPaymentIntent($amount, $customMetaData);
|
||||
return $this->format($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $paymentId
|
||||
* @return array
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function retrievePaymentIntent($paymentId): array {
|
||||
try {
|
||||
return $this->api->order->fetch($paymentId);
|
||||
} catch (Throwable $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $currency
|
||||
* @param $amount
|
||||
* @return float|int
|
||||
*/
|
||||
public function minimumAmountValidation($currency, $amount) {
|
||||
return match ($currency) {
|
||||
"BHD", "IQD", "JOD", "KWD", "OMR", "TND" => $amount * 1000,
|
||||
"AED", "ALL", "AMD", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", "BGN", "BMD", "BND", "BOB", "BRL", "BSD", "BTN", "BWP", "BZD", "CAD", "CHF",
|
||||
"CNY", "COP", "CRC", "CUP", "CVE", "CZK", "DKK", "DOP", "DZD", "EGP", "ETB", "EUR", "FJD", "GBP", "GHS", "GIP", "GMD", "GTQ", "GYD", "HKD", "HNL",
|
||||
"HTG", "HUF", "IDR", "ILS", "INR", "JMD", "KES", "KGS", "KHR", "KYD", "KZT", "LAK", "LKR", "LRD", "LSL", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT",
|
||||
"MOP", "MUR", "MVR", "MWK", "MXN", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "PEN", "PGK", "PHP", "PKR", "PLN", "QAR", "RON", "RSD",
|
||||
"RUB", "SAR", "SCR", "SEK", "SGD", "SLL", "SOS", "SSP", "SVC", "SZL", "THB", "TTD", "TWD", "TZS", "UAH", "USD", "UYU", "UZS", "XCD", "YER", "ZAR", "ZMW" => $amount * 100,
|
||||
"BIF", "CLP", "DJF", "GNF", "ISK", "JPY", "KMF", "KRW", "PYG", "RWF", "UGX", "VND", "VUV", "XAF", "XOF", "XPF", "HRK" => $amount,
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $paymentIntent
|
||||
* @return array
|
||||
*/
|
||||
private function format($paymentIntent) {
|
||||
return $this->formatPaymentIntent($paymentIntent->id, $paymentIntent->amount, $paymentIntent->currency, $paymentIntent->status, $paymentIntent->notes->toArray(), $paymentIntent->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @param $amount
|
||||
* @param $currency
|
||||
* @param $status
|
||||
* @param $metadata
|
||||
* @param $paymentIntent
|
||||
* @return array
|
||||
*/
|
||||
public function formatPaymentIntent($id, $amount, $currency, $status, $metadata, $paymentIntent): array {
|
||||
return [
|
||||
'id' => $id,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'metadata' => $metadata,
|
||||
'status' => match ($status) {
|
||||
"failed" => "failed",//NOTE : Failed status is not known, please test the failure status
|
||||
"created", "attempted" => "pending",
|
||||
"paid" => "succeed",
|
||||
|
||||
},
|
||||
'payment_gateway_response' => $paymentIntent
|
||||
];
|
||||
}
|
||||
}
|
||||
184
app/Services/Payment/StripePayment.php
Normal file
184
app/Services/Payment/StripePayment.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Payment;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Services\CurrencyFormatterService;
|
||||
use Stripe\Exception\ApiErrorException;
|
||||
use Stripe\PaymentIntent;
|
||||
use Stripe\StripeClient;
|
||||
|
||||
class StripePayment implements PaymentInterface {
|
||||
private StripeClient $stripe;
|
||||
private string $currencyCode;
|
||||
|
||||
/**
|
||||
* StripePayment constructor.
|
||||
* @param $secretKey
|
||||
* @param $currencyCode
|
||||
*/
|
||||
public function __construct($secretKey, $currencyCode) {
|
||||
// Call Stripe Class and Create Payment Intent
|
||||
$this->stripe = new StripeClient($secretKey);
|
||||
$this->currencyCode = $currencyCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $amount
|
||||
* @param $customMetaData
|
||||
* @return PaymentIntent
|
||||
* @throws ApiErrorException
|
||||
*/
|
||||
public function createPaymentIntent($amount, $customMetaData) {
|
||||
try {
|
||||
$amount = $this->minimumAmountValidation($this->currencyCode, $amount);
|
||||
$zeroDecimalCurrencies = [
|
||||
'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG',
|
||||
'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF'
|
||||
];
|
||||
|
||||
// if (!in_array($this->currencyCode, $zeroDecimalCurrencies)) {
|
||||
// $amount *= 100;
|
||||
// }
|
||||
return $this->stripe->paymentIntents->create(
|
||||
[
|
||||
'amount' => $amount,
|
||||
'currency' => $this->currencyCode,
|
||||
'metadata' => $customMetaData,
|
||||
// 'description' => 'Fees Payment',
|
||||
// 'shipping' => [
|
||||
// 'name' => 'Jenny Rosen',
|
||||
// 'address' => [
|
||||
// 'line1' => '510 Townsend St',
|
||||
// 'postal_code' => '98140',
|
||||
// 'city' => 'San Francisco',
|
||||
// 'state' => 'CA',
|
||||
// 'country' => 'US',
|
||||
// ],
|
||||
// ],
|
||||
]
|
||||
);
|
||||
|
||||
} catch (ApiErrorException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $amount
|
||||
* @param $customMetaData
|
||||
* @return array
|
||||
* @throws ApiErrorException
|
||||
*/
|
||||
public function createAndFormatPaymentIntent($amount, $customMetaData): array {
|
||||
$paymentIntent = $this->createPaymentIntent($amount, $customMetaData);
|
||||
return $this->format($paymentIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $paymentId
|
||||
* @return array
|
||||
* @throws ApiErrorException
|
||||
*/
|
||||
public function retrievePaymentIntent($paymentId): array {
|
||||
try {
|
||||
return $this->format($this->stripe->paymentIntents->retrieve($paymentId));
|
||||
} catch (ApiErrorException $e) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $paymentIntent
|
||||
* @return array
|
||||
*/
|
||||
public function format($paymentIntent) {
|
||||
return $this->formatPaymentIntent($paymentIntent->id, $paymentIntent->amount, $paymentIntent->currency, $paymentIntent->status, $paymentIntent->metadata, $paymentIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @param $amount
|
||||
* @param $currency
|
||||
* @param $status
|
||||
* @param $metadata
|
||||
* @param $paymentIntent
|
||||
* @return array
|
||||
*/
|
||||
public function formatPaymentIntent($id, $amount, $currency, $status, $metadata, $paymentIntent): array {
|
||||
$formatter = app(CurrencyFormatterService::class);
|
||||
$iso_code = Setting::where('name', 'currency_iso_code')->value('value');
|
||||
$symbol = Setting::where('name', 'currency_symbol')->value('value');
|
||||
$position = Setting::where('name', 'currency_symbol_position')->value('value');
|
||||
|
||||
$currency = (object) [
|
||||
'iso_code' => $iso_code,
|
||||
'symbol' => $symbol,
|
||||
'symbol_position' => $position,
|
||||
];
|
||||
$displayAmount = $amount;
|
||||
// If it's NOT a zero-decimal currency, divide by 100 for the formatter
|
||||
// if (!in_array($iso_code, ['BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG', 'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF', 'ISK'])) {
|
||||
$displayAmount = $amount * 100;
|
||||
// }
|
||||
|
||||
|
||||
$formatted_final_price = $formatter->formatPrice($displayAmount ?? 0, $currency);
|
||||
return [
|
||||
'id' => $paymentIntent->id,
|
||||
'amount' => $paymentIntent->amount,
|
||||
'formatted_price' => $formatted_final_price,
|
||||
'currency' => $paymentIntent->currency,
|
||||
'metadata' => $paymentIntent->metadata,
|
||||
'status' => match ($paymentIntent->status) {
|
||||
"canceled" => "failed",
|
||||
"succeeded" => "succeed",
|
||||
"processing", "requires_action", "requires_capture", "requires_confirmation", "requires_payment_method" => "pending",
|
||||
},
|
||||
'payment_gateway_response' => $paymentIntent
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $currency
|
||||
* @param $amount
|
||||
* @return float|int
|
||||
*/
|
||||
public function minimumAmountValidation($currency, $amount) {
|
||||
$minimumAmountMap = [
|
||||
'USD' => 0.50, 'EUR' => 0.50, 'INR' => 0.50, 'NZD' => 0.50, 'SGD' => 0.50,
|
||||
'BRL' => 0.50, 'CAD' => 0.50, 'AUD' => 0.50, 'CHF' => 0.50,
|
||||
'AED' => 2.00, 'PLN' => 2.00, 'RON' => 2.00,
|
||||
'BGN' => 1.00, 'CZK' => 15.00, 'DKK' => 2.50,
|
||||
'GBP' => 0.30, 'HKD' => 4.00, 'HUF' => 175.00,
|
||||
'JPY' => 50, 'MXN' => 10, 'THB' => 10, 'MYR' => 2,
|
||||
'NOK' => 3.00, 'SEK' => 3.00, 'XAF' => 100,
|
||||
'ISK' => 100 // ISK minimum is usually higher
|
||||
];
|
||||
|
||||
$zeroDecimalCurrencies = [
|
||||
'BIF', 'CLP', 'DJF', 'GNF', 'JPY', 'KMF', 'KRW', 'MGA', 'PYG',
|
||||
'RWF', 'UGX', 'VND', 'VUV', 'XAF', 'XOF', 'XPF', 'ISK' // Added ISK here
|
||||
];
|
||||
|
||||
$minimumAmount = $minimumAmountMap[$currency] ?? 0.50;
|
||||
|
||||
if (!in_array($currency, $zeroDecimalCurrencies)) {
|
||||
// Standard Currencies (USD, INR, etc.)
|
||||
$minimumAmount *= 100;
|
||||
$amount = (int)round($amount * 100);
|
||||
} else {
|
||||
// Zero-decimal Currencies
|
||||
if ($currency === 'ISK') {
|
||||
// Special Rule for ISK: Must be divisible by 100
|
||||
$amount = (int)round($amount / 100) * 100;
|
||||
if ($amount < 100) $amount = 100;
|
||||
} else {
|
||||
$amount = (int)$amount;
|
||||
}
|
||||
}
|
||||
|
||||
return max($amount, (int)$minimumAmount);
|
||||
}
|
||||
}
|
||||
255
app/Services/ResponseService.php
Normal file
255
app/Services/ResponseService.php
Normal file
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
class ResponseService
|
||||
{
|
||||
/**
|
||||
* @return Application|RedirectResponse|Redirector|true
|
||||
*/
|
||||
public static function noPermissionThenRedirect($permission)
|
||||
{
|
||||
if (! Auth::user()->can($permission)) {
|
||||
return redirect(route('home'))->withErrors([
|
||||
'message' => trans("You Don't have enough permissions"),
|
||||
])->send();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true
|
||||
*/
|
||||
public static function noPermissionThenSendJson($permission)
|
||||
{
|
||||
if (! Auth::user()->can($permission)) {
|
||||
self::errorResponse("You Don't have enough permissions");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Application|\Illuminate\Foundation\Application|RedirectResponse|Redirector|true
|
||||
*/
|
||||
// Check user role
|
||||
public static function noRoleThenRedirect($role)
|
||||
{
|
||||
if (! Auth::user()->hasRole($role)) {
|
||||
return redirect(route('home'))->withErrors([
|
||||
'message' => trans("You Don't have enough permissions"),
|
||||
])->send();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|Application|\Illuminate\Foundation\Application|RedirectResponse|Redirector
|
||||
*/
|
||||
public static function noAnyRoleThenRedirect(array $role)
|
||||
{
|
||||
if (! Auth::user()->hasAnyRole($role)) {
|
||||
return redirect(route('home'))->withErrors([
|
||||
'message' => trans("You Don't have enough permissions"),
|
||||
])->send();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @param $role
|
||||
// * @return true
|
||||
// */
|
||||
// public static function noRoleThenSendJson($role)
|
||||
// {
|
||||
// if (!Auth::user()->hasRole($role)) {
|
||||
// self::errorResponse("You Don't have enough permissions");
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
|
||||
/**
|
||||
* @param $feature
|
||||
* @return RedirectResponse|true
|
||||
*/
|
||||
// Check Feature
|
||||
// public static function noFeatureThenRedirect($feature) {
|
||||
// if (Auth::user()->school_id && !app(FeaturesService::class)->hasFeature($feature)) {
|
||||
// return redirect()->back()->withErrors([
|
||||
// 'message' => trans('Purchase') . " " . $feature . " " . trans("to Continue using this functionality")
|
||||
// ])->send();
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// public static function noFeatureThenSendJson($feature) {
|
||||
// if (Auth::user()->school_id && !app(FeaturesService::class)->hasFeature($feature)) {
|
||||
// self::errorResponse(trans('Purchase') . " " . $feature . " " . trans("to Continue using this functionality"));
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
|
||||
/**
|
||||
* If User don't have any of the permission that is specified in Array then Redirect will happen
|
||||
*
|
||||
* @return RedirectResponse|true
|
||||
*/
|
||||
public static function noAnyPermissionThenRedirect(array $permissions)
|
||||
{
|
||||
if (! Auth::user()->canany($permissions)) {
|
||||
return redirect()->back()->withErrors([
|
||||
'message' => trans("You Don't have enough permissions"),
|
||||
])->send();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* If User don't have any of the permission that is specified in Array then Json Response will be sent
|
||||
*
|
||||
* @return true
|
||||
*/
|
||||
public static function noAnyPermissionThenSendJson(array $permissions)
|
||||
{
|
||||
if (! Auth::user()->canany($permissions)) {
|
||||
self::errorResponse("You Don't have enough permissions");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null $data
|
||||
* @param null $code
|
||||
*/
|
||||
public static function successResponse(?string $message = 'Success', $data = null, array $customData = [], $code = null): void
|
||||
{
|
||||
response()->json(array_merge([
|
||||
'error' => false,
|
||||
'message' => trans($message),
|
||||
'data' => $data,
|
||||
'code' => $code ?? config('constants.RESPONSE_CODE.SUCCESS'),
|
||||
], $customData))->send();
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Application|\Illuminate\Foundation\Application|RedirectResponse|Redirector
|
||||
*/
|
||||
public static function successRedirectResponse(string $message = 'success', $url = null)
|
||||
{
|
||||
return isset($url) ? redirect($url)->with([
|
||||
'success' => trans($message),
|
||||
])->send() : redirect()->back()->with([
|
||||
'success' => trans($message),
|
||||
])->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $message - Pass the Translatable Field
|
||||
* @param null $data
|
||||
* @param string $code
|
||||
* @param null $e
|
||||
* @return void
|
||||
*/
|
||||
public static function errorResponse(string $message = 'Error Occurred', $data = null, string|int|null $code = null, $e = null)
|
||||
{
|
||||
response()->json([
|
||||
'error' => true,
|
||||
'message' => trans($message),
|
||||
'data' => $data,
|
||||
'code' => $code ?? config('constants.RESPONSE_CODE.EXCEPTION_ERROR'),
|
||||
'details' => (! empty($e) && is_object($e)) ? $e->getMessage().' --> '.$e->getFile().' At Line : '.$e->getLine() : '',
|
||||
])->send();
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* return keyword should, must be used wherever this function is called.
|
||||
*
|
||||
* @param string|string[] $message
|
||||
* @param null $input
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
public static function errorRedirectResponse(string|array $message = 'Error Occurred', $url = 'back', $input = null)
|
||||
{
|
||||
return $url == 'back' ? redirect()->back()->with([
|
||||
'errors' => trans($message),
|
||||
])->withInput($input) : redirect($url)->with([
|
||||
'errors' => trans($message),
|
||||
])->withInput($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null $data
|
||||
* @param null $code
|
||||
* @return void
|
||||
*/
|
||||
public static function warningResponse(string $message = 'Error Occurred', $data = null, $code = null)
|
||||
{
|
||||
response()->json([
|
||||
'error' => false,
|
||||
'warning' => true,
|
||||
'code' => $code,
|
||||
'message' => trans($message),
|
||||
'data' => $data,
|
||||
])->send();
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null $data
|
||||
* @return void
|
||||
*/
|
||||
public static function validationError(string $message = 'Error Occurred', $data = null)
|
||||
{
|
||||
self::errorResponse($message, $data, config('constants.RESPONSE_CODE.VALIDATION_ERROR'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function validationErrorRedirect(string $message = 'Error Occurred')
|
||||
{
|
||||
self::errorRedirectResponse(route('custom-fields.create'), $message);
|
||||
exit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function logErrorResponse(Throwable|Exception $e, string $logMessage = ' ', string $responseMessage = 'Error Occurred', bool $jsonResponse = true)
|
||||
{
|
||||
$token = request()->bearerToken();
|
||||
|
||||
Log::error($logMessage.' '.$e->getMessage().'---> '.$e->getFile().' At Line : '.$e->getLine()."\n\n".request()->method().' : '.request()->fullUrl()."\nToken : ".$token."\nParams : ", request()->all());
|
||||
if ($jsonResponse && config('app.debug')) {
|
||||
self::errorResponse($responseMessage, null, null, $e);
|
||||
}
|
||||
}
|
||||
|
||||
public static function logErrorRedirect($e, string $logMessage = ' ', string $responseMessage = 'Error Occurred', bool $jsonResponse = true)
|
||||
{
|
||||
Log::error($logMessage.' '.$e->getMessage().'---> '.$e->getFile().' At Line : '.$e->getLine());
|
||||
if ($jsonResponse && config('app.debug')) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public static function errorRedirectWithToast(string $message, $input = null)
|
||||
{
|
||||
return redirect()->back()->with('error', $message)->withInput($input);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user