2026-01-24 14:03:11 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
|
|
2026-04-26 20:09:21 +00:00
|
|
|
use App\Models\Cnt;
|
2026-01-24 14:03:11 +00:00
|
|
|
use App\Models\User;
|
|
|
|
|
use App\Models\Customer;
|
|
|
|
|
use App\Models\Setting;
|
|
|
|
|
use App\Models\Message;
|
2026-07-04 11:55:10 +03:30
|
|
|
use App\Exceptions\SmsBlockedException;
|
2026-06-07 11:52:57 +03:30
|
|
|
use App\Services\ConfigService;
|
2026-07-04 11:55:10 +03:30
|
|
|
use App\Services\SmsBilling\SmsGatewayService;
|
2026-01-24 14:03:11 +00:00
|
|
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
|
|
|
|
use Illuminate\Foundation\Bus\DispatchesJobs;
|
|
|
|
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
|
|
|
|
use Illuminate\Routing\Controller as BaseController;
|
2026-04-19 17:19:25 +03:30
|
|
|
use Illuminate\Support\Facades\Storage;
|
2026-01-24 14:03:11 +00:00
|
|
|
use Illuminate\Support\Arr;
|
|
|
|
|
use Illuminate\Support\Facades\Http;
|
2026-04-19 17:19:25 +03:30
|
|
|
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
|
|
|
|
|
2026-01-24 14:03:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Controller extends BaseController
|
|
|
|
|
{
|
|
|
|
|
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
|
|
|
|
|
public function resultCreate($id){
|
|
|
|
|
return response()->json(['id'=>$id] , 201);
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* add paginate to list
|
|
|
|
|
* @param array $list
|
|
|
|
|
* @param object $table
|
|
|
|
|
* @return array mixed
|
|
|
|
|
*/
|
|
|
|
|
protected function addPaginate($table,$list){
|
|
|
|
|
$list = $this->addLimitPage($list);
|
|
|
|
|
$list['count'] = $table->count();
|
|
|
|
|
$list['rows'] = $table->offset( $list['page']*$list['limit'])
|
|
|
|
|
->limit($list['limit'])
|
|
|
|
|
->get();
|
|
|
|
|
$list['page'] = (int)$list['page'];
|
|
|
|
|
$list['limit'] = (int)$list['limit'];
|
|
|
|
|
return $list;
|
|
|
|
|
}
|
|
|
|
|
/**
|
|
|
|
|
* add page and limit to list
|
|
|
|
|
* @param $list
|
|
|
|
|
* @return mixed
|
|
|
|
|
*/
|
|
|
|
|
protected function addLimitPage($list){
|
|
|
|
|
if (!isset($list['page'])){
|
|
|
|
|
$list['page'] = 0;
|
|
|
|
|
}
|
|
|
|
|
if (!isset($list['limit'])){
|
|
|
|
|
$list['limit'] = 5;
|
|
|
|
|
}
|
|
|
|
|
if ($list['limit']>50){
|
|
|
|
|
$list['limit'] = 50;
|
|
|
|
|
}
|
|
|
|
|
return $list;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 20:38:48 +03:30
|
|
|
/** مبلغ ریالی در پیامک — جداکننده هزارگان */
|
|
|
|
|
public static function formatSmsRialAmount(float|int|string $value): string
|
|
|
|
|
{
|
|
|
|
|
return number_format(abs((float) $value), 0, '.', ',');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** وزن طلا در پیامک — تا ۳ رقم اعشار، بدون گرد کردن به صفر برای زیر ۱ گرم */
|
|
|
|
|
public static function formatSmsGoldWeight(float|int|string $value): string
|
|
|
|
|
{
|
|
|
|
|
$formatted = rtrim(rtrim(number_format(abs((float) $value), 3, '.', ''), '0'), '.');
|
|
|
|
|
|
|
|
|
|
return $formatted !== '' ? $formatted : '0';
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-23 22:05:47 +03:30
|
|
|
/**
|
|
|
|
|
* گزینههای Guzzle/cURL — فقط وقتی افزونه curl و ثابتها موجود باشند.
|
|
|
|
|
*
|
|
|
|
|
* @param array<int, mixed> $curlOptions
|
|
|
|
|
* @return array<string, array<int, mixed>>
|
|
|
|
|
*/
|
|
|
|
|
public static function httpClientOptions(array $curlOptions = []): array
|
|
|
|
|
{
|
|
|
|
|
$curl = $curlOptions;
|
|
|
|
|
|
|
|
|
|
if (defined('CURLOPT_IPRESOLVE') && defined('CURL_IPRESOLVE_V4')) {
|
|
|
|
|
$resolveOpt = (int) constant('CURLOPT_IPRESOLVE');
|
|
|
|
|
if (! array_key_exists($resolveOpt, $curl)) {
|
|
|
|
|
$curl[$resolveOpt] = (int) constant('CURL_IPRESOLVE_V4');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $curl === [] ? [] : ['curl' => $curl];
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 14:03:11 +00:00
|
|
|
public static function Send($to , $msg, $is_customer = true)
|
|
|
|
|
{
|
|
|
|
|
$r = self::SendMessage($to , $msg, $is_customer);
|
|
|
|
|
// if ($r == 200){
|
|
|
|
|
// Sms::set($to,$msg , $is_customer);
|
|
|
|
|
// }
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 22:22:01 +03:30
|
|
|
/** آیا این نوع پیامک اعلان روشن است؟ پیشفرض true اگر کلید در settings نباشد. */
|
|
|
|
|
public static function isSmsNotifEnabled(string $key, bool $default = true): bool
|
|
|
|
|
{
|
|
|
|
|
return app(ConfigService::class)->isSmsNotifEnabled($key, $default);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* خرید/فروش فقط بعد از احراز هویت (is_legal ≠ 0).
|
|
|
|
|
* جدا از active / auto_active_customer که فقط ورود به اپ را کنترل میکنند.
|
|
|
|
|
*/
|
|
|
|
|
public static function assertCustomerIdentityVerified(?Customer $customer): void
|
|
|
|
|
{
|
|
|
|
|
if (!$customer || (int) ($customer->is_legal ?? 0) === 0) {
|
|
|
|
|
throw new UnprocessableEntityHttpException('لطفا ابتدا فرآیند احراز هویت خود را تکمیل کنید');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 14:03:11 +00:00
|
|
|
public static function SendMessage($to, $msg ,$is_send = false,$force_method ='null')
|
|
|
|
|
{
|
|
|
|
|
$credentials = [
|
|
|
|
|
"telegram" => [
|
|
|
|
|
"token" => self::Config('telegram_bot_token')
|
|
|
|
|
],
|
2026-04-19 17:19:25 +03:30
|
|
|
"bale" => [
|
|
|
|
|
"token" => self::Config('bale_bot_token')
|
|
|
|
|
],
|
2026-01-24 14:03:11 +00:00
|
|
|
"sms" => [
|
|
|
|
|
'user_name' => 'ysms1362',
|
2026-02-16 09:29:37 +00:00
|
|
|
'password' => 'tL4w0PfNy2()',
|
2026-01-24 14:03:11 +00:00
|
|
|
'sender' => '3000164545',
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
$platforms = [];
|
|
|
|
|
$user = User::where('mobile', $to)->first() ?: Customer::where('mobile', $to)->first();
|
2026-07-18 22:22:01 +03:30
|
|
|
|
|
|
|
|
// بله و پیامک مستقلاند — موفقیت یکی، دیگری را قطع نمیکند
|
2026-04-19 17:19:25 +03:30
|
|
|
if ($user) {
|
2026-06-14 21:09:43 +03:30
|
|
|
$baleToken = trim((string) ($credentials['bale']['token'] ?? ''));
|
|
|
|
|
$baleChatId = trim((string) ($user->bale ?? ''));
|
|
|
|
|
if ($baleToken !== '' && $baleChatId !== '') {
|
2026-04-19 17:19:25 +03:30
|
|
|
try {
|
2026-06-14 21:09:43 +03:30
|
|
|
$baleResponse = Http::withoutVerifying()->get(
|
|
|
|
|
"https://tapi.bale.ai/bot{$baleToken}/sendMessage",
|
|
|
|
|
[
|
|
|
|
|
'chat_id' => $baleChatId,
|
|
|
|
|
'text' => "جناب {$user->name} \n{$msg}",
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
$balePayload = $baleResponse->json();
|
|
|
|
|
if ($baleResponse->successful() && ($balePayload['ok'] ?? false) === true) {
|
|
|
|
|
$platforms[] = "bale:{$baleChatId}";
|
2026-07-18 22:22:01 +03:30
|
|
|
} else {
|
|
|
|
|
$platforms[] = "bale_failed:{$baleChatId}";
|
2026-06-14 21:09:43 +03:30
|
|
|
}
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
report($e);
|
2026-07-18 22:22:01 +03:30
|
|
|
$platforms[] = "bale_error:{$baleChatId}";
|
2026-04-19 17:19:25 +03:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-24 14:03:11 +00:00
|
|
|
|
2026-07-18 22:22:01 +03:30
|
|
|
try {
|
|
|
|
|
$gateway = app(SmsGatewayService::class);
|
2026-06-23 22:05:47 +03:30
|
|
|
|
2026-07-18 22:22:01 +03:30
|
|
|
if ($force_method == "sms") {
|
|
|
|
|
$smsResult = $gateway->sendOrThrow($to, trim($msg), 'notification');
|
|
|
|
|
} else {
|
|
|
|
|
$smsResult = $gateway->send($to, trim($msg), 'notification');
|
2026-06-23 22:05:47 +03:30
|
|
|
}
|
2026-07-18 22:22:01 +03:30
|
|
|
|
|
|
|
|
if ($smsResult->blocked) {
|
|
|
|
|
$platforms[] = "blocked:{$to}";
|
|
|
|
|
} elseif ($smsResult->sent && $smsResult->provider) {
|
|
|
|
|
$platforms[] = "{$smsResult->provider}:{$to}";
|
|
|
|
|
} else {
|
|
|
|
|
$platforms[] = "sms_failed:{$to}";
|
|
|
|
|
}
|
|
|
|
|
} catch (SmsBlockedException $e) {
|
|
|
|
|
throw $e;
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
report($e);
|
|
|
|
|
$platforms[] = "sms_error:{$to}";
|
2026-02-22 15:06:01 +03:30
|
|
|
}
|
2026-01-24 14:03:11 +00:00
|
|
|
|
|
|
|
|
Message::create([
|
|
|
|
|
'sms' => $msg,
|
|
|
|
|
'receiver' => $to,
|
2026-07-18 22:22:01 +03:30
|
|
|
'platforms' => implode(', ', $platforms),
|
2026-01-24 14:03:11 +00:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
public static function SendTelegram($to , $msg)
|
|
|
|
|
{
|
|
|
|
|
// "chatId" => 504951108,
|
|
|
|
|
// "chatId" => 5870340227, fathi
|
|
|
|
|
$response = Http::withHeaders([
|
|
|
|
|
'x-secret-key' => 'PvQM36mgk2FUELHjcm4YG49Nu56uQxot',
|
|
|
|
|
])->post('https://tele.ygram.ir/send-message', ["message" => $msg, "chatId" => $to, "botToken" => 'Paramisgold_bot']);
|
|
|
|
|
return $response->json();
|
|
|
|
|
}
|
|
|
|
|
public static function Config($name)
|
|
|
|
|
{
|
2026-06-05 17:43:02 +03:30
|
|
|
$row = Setting::where('key', $name)->first();
|
|
|
|
|
|
|
|
|
|
if ($row && $row->is_active && $row->value !== null && $row->value !== '') {
|
|
|
|
|
return $row->value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
2026-01-24 14:03:11 +00:00
|
|
|
}
|
2026-04-19 17:19:25 +03:30
|
|
|
|
2026-06-03 21:37:05 +03:30
|
|
|
protected static function metalTypeSettingValue(): ?string
|
2026-06-03 20:24:07 +03:30
|
|
|
{
|
|
|
|
|
$metalType = Setting::where('key', 'metal_type')->value('value');
|
2026-06-03 21:37:05 +03:30
|
|
|
if ($metalType === null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-06-03 20:24:07 +03:30
|
|
|
|
2026-06-03 21:37:05 +03:30
|
|
|
$metalType = trim((string) $metalType);
|
|
|
|
|
|
|
|
|
|
return $metalType !== '' ? $metalType : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** برچسب موجودی کیف اصلی: مقدار setting یا «طلا» */
|
|
|
|
|
public static function metalTypeLabel(): string
|
|
|
|
|
{
|
|
|
|
|
return self::metalTypeSettingValue() ?? 'طلا';
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 01:17:34 +03:30
|
|
|
public static function metalIdLabel(int $metalId): string
|
|
|
|
|
{
|
|
|
|
|
return match ($metalId) {
|
|
|
|
|
1 => 'طلا',
|
|
|
|
|
2 => 'نقره',
|
|
|
|
|
3 => 'مس',
|
|
|
|
|
default => self::metalTypeLabel(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** برچسب فلز کیف اصلی از `main_wallet_products` یا setting */
|
|
|
|
|
public static function mainWalletMetalLabel(): string
|
|
|
|
|
{
|
|
|
|
|
$product = app(ConfigService::class)->mainWalletRepresentativeProduct();
|
|
|
|
|
if ($product && (int) ($product->metal_id ?? 0) > 0) {
|
|
|
|
|
return self::metalIdLabel((int) $product->metal_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return self::metalTypeLabel();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-24 01:32:32 +03:30
|
|
|
/** عنوان محصول(ات) کیف اصلی از `main_wallet_products` */
|
|
|
|
|
public static function mainWalletProductLabel(): string
|
|
|
|
|
{
|
|
|
|
|
$labels = app(ConfigService::class)->mainWalletProductLabels();
|
|
|
|
|
|
|
|
|
|
return $labels !== '' ? $labels : self::mainWalletMetalLabel();
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 21:37:05 +03:30
|
|
|
/** برچسب معاملات آبشده: مقدار setting یا «آبشده» */
|
|
|
|
|
public static function meltedGoldLabel(): string
|
|
|
|
|
{
|
|
|
|
|
return self::metalTypeSettingValue() ?? 'آبشده';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function cashTransactionTitles(): array
|
|
|
|
|
{
|
|
|
|
|
return ['برداشت وجه نقد', 'واریز وجه نقد'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function legacyMeltedTransactionTitles(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
|
|
|
|
'واریز طلای ابشده',
|
|
|
|
|
'واریز طلای آبشده',
|
|
|
|
|
'برداشت طلای ابشده',
|
|
|
|
|
'برداشت طلای آبشده',
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected static function metalLabelVariants(): array
|
|
|
|
|
{
|
|
|
|
|
return array_values(array_unique([
|
|
|
|
|
self::meltedGoldLabel(),
|
|
|
|
|
self::metalTypeLabel(),
|
|
|
|
|
'طلا',
|
|
|
|
|
'آبشده',
|
|
|
|
|
]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function genericMetalTransactionTitles(): array
|
|
|
|
|
{
|
2026-06-24 01:17:34 +03:30
|
|
|
$titles = array_merge(self::legacyMeltedTransactionTitles(), [
|
|
|
|
|
'واریز طلایی',
|
|
|
|
|
'برداشت طلایی',
|
|
|
|
|
]);
|
2026-06-03 21:37:05 +03:30
|
|
|
|
|
|
|
|
foreach (self::metalLabelVariants() as $label) {
|
|
|
|
|
$titles[] = "موجودی {$label}";
|
2026-06-24 01:17:34 +03:30
|
|
|
$titles[] = "واریز {$label}";
|
2026-06-03 21:37:05 +03:30
|
|
|
$titles[] = "برداشت {$label}";
|
|
|
|
|
$titles[] = "خرید {$label}";
|
|
|
|
|
$titles[] = "فروش {$label}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return array_values(array_unique($titles));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function isCashTransactionTitle(?string $title): bool
|
|
|
|
|
{
|
|
|
|
|
return $title !== null && in_array($title, self::cashTransactionTitles(), true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function isGenericMetalTransactionTitle(?string $title): bool
|
|
|
|
|
{
|
|
|
|
|
if ($title === null || self::isCashTransactionTitle($title)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return in_array($title, self::genericMetalTransactionTitles(), true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-30 10:24:42 +03:30
|
|
|
public static function rialDepositTitle(): string
|
|
|
|
|
{
|
|
|
|
|
return 'واریز ریالی';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function rialWithdrawTitle(): string
|
|
|
|
|
{
|
|
|
|
|
return 'برداشت ریالی';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* type و title تراکنش ریالی — بر اساس علامت cash (دادههای قدیمی گاهی type=4 با cash مثبت دارند).
|
|
|
|
|
*/
|
|
|
|
|
public static function normalizeRialTransactionPresentation($record, bool $persist = true): void
|
|
|
|
|
{
|
|
|
|
|
$type = (int) ($record->type ?? 0);
|
|
|
|
|
if (!in_array($type, [3, 4], true)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$cash = (float) ($record->cash ?? 0);
|
|
|
|
|
$correctType = $cash >= 0 ? 3 : 4;
|
|
|
|
|
$correctTitle = $correctType === 3 ? self::rialDepositTitle() : self::rialWithdrawTitle();
|
|
|
|
|
|
|
|
|
|
$dirty = false;
|
|
|
|
|
if ($correctType !== $type) {
|
|
|
|
|
$record->type = $correctType;
|
|
|
|
|
$dirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (trim((string) ($record->title ?? '')) !== $correctTitle) {
|
|
|
|
|
$record->title = $correctTitle;
|
|
|
|
|
$dirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($dirty && $persist && $record instanceof \Rooyesh\Wallet\models\Transaction) {
|
|
|
|
|
$record->save();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 21:37:05 +03:30
|
|
|
public static function productBuyTitle(string $productTitle): string
|
|
|
|
|
{
|
|
|
|
|
return 'خرید ' . $productTitle;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function productSellTitle(string $productTitle): string
|
|
|
|
|
{
|
|
|
|
|
return 'فروش ' . $productTitle;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 11:17:07 +03:30
|
|
|
public static function mainMetalBuyTitle(): string
|
|
|
|
|
{
|
2026-06-24 01:32:32 +03:30
|
|
|
return self::productBuyTitle(self::mainWalletProductLabel());
|
2026-06-09 11:17:07 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function mainMetalSellTitle(): string
|
|
|
|
|
{
|
2026-06-24 01:32:32 +03:30
|
|
|
return self::productSellTitle(self::mainWalletProductLabel());
|
2026-06-24 01:17:34 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function mainMetalDepositTitle(): string
|
|
|
|
|
{
|
2026-06-24 01:32:32 +03:30
|
|
|
return 'واریز ' . self::mainWalletProductLabel();
|
2026-06-24 01:17:34 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function mainMetalWithdrawTitle(): string
|
|
|
|
|
{
|
2026-06-24 01:32:32 +03:30
|
|
|
return 'برداشت ' . self::mainWalletProductLabel();
|
2026-06-09 11:17:07 +03:30
|
|
|
}
|
|
|
|
|
|
2026-06-10 17:31:38 +03:30
|
|
|
/**
|
|
|
|
|
* آیا محصول به کیف اصلی متصل است؟ (setting: main_wallet_products)
|
|
|
|
|
* موجودی این محصولات = gold کیف اصلی؛ تراکنشهایشان روی کیف اصلی مینشیند.
|
|
|
|
|
*/
|
|
|
|
|
public static function isMainWalletProduct(?int $productId): bool
|
|
|
|
|
{
|
|
|
|
|
if (!$productId) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return app(ConfigService::class)->isMainWalletProduct((int) $productId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* شناسه کیف مؤثر برای تراکنش محصول.
|
|
|
|
|
* محصولات متصل به کیف اصلی → null (کیف اصلی)؛ بقیه → کیف محصول خودشان.
|
|
|
|
|
*/
|
|
|
|
|
public static function effectiveProductWalletId(?int $productId): ?int
|
|
|
|
|
{
|
|
|
|
|
return self::isMainWalletProduct($productId) ? null : $productId;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 12:43:41 +03:30
|
|
|
/** واحد وزن معاملات فلز — از ستینگ unit؛ پیشفرض گرم */
|
2026-06-09 11:17:07 +03:30
|
|
|
public static function tradeWeightUnit(): string
|
|
|
|
|
{
|
2026-06-09 12:43:41 +03:30
|
|
|
return self::resolveTradeWeightUnit(
|
|
|
|
|
Setting::where('key', 'unit')->value('value')
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-06-09 11:17:07 +03:30
|
|
|
|
2026-06-09 12:43:41 +03:30
|
|
|
public static function resolveTradeWeightUnit(?string $value): string
|
|
|
|
|
{
|
|
|
|
|
$unit = trim((string) $value);
|
|
|
|
|
|
|
|
|
|
if ($unit === 'کیلوگرم' || $unit === 'kilogram' || $unit === 'kg') {
|
|
|
|
|
return 'کیلوگرم';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'گرم';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function weightToGrams(float $value, ?string $fromUnit = null): float
|
|
|
|
|
{
|
|
|
|
|
$from = $fromUnit ?? self::tradeWeightUnit();
|
|
|
|
|
|
|
|
|
|
return $from === 'کیلوگرم' ? $value * 1000 : $value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function gramsToTradeUnit(float $grams): float
|
|
|
|
|
{
|
|
|
|
|
return self::tradeWeightUnit() === 'کیلوگرم' ? $grams / 1000 : $grams;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function productStorageWeightUnit($product): string
|
|
|
|
|
{
|
|
|
|
|
$unit = trim((string) (is_object($product) ? ($product->unit ?? '') : ($product['unit'] ?? '')));
|
|
|
|
|
|
|
|
|
|
if (self::isWeightBasedProductUnit($unit)) {
|
|
|
|
|
return $unit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'گرم';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function productLineWeightToGrams($product, $quantity): float
|
|
|
|
|
{
|
|
|
|
|
$groupType = is_object($product) ? ($product->group_type ?? '') : ($product['group_type'] ?? '');
|
|
|
|
|
if ((string) $groupType === 'سکه') {
|
|
|
|
|
return 0.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$qty = (float) $quantity;
|
|
|
|
|
$unit = trim((string) (is_object($product) ? ($product->unit ?? '') : ($product['unit'] ?? '')));
|
|
|
|
|
$amount = (float) (is_object($product) ? ($product->amount ?? 0) : ($product['amount'] ?? 0));
|
|
|
|
|
|
|
|
|
|
if (self::isWeightBasedProductUnit($unit)) {
|
|
|
|
|
return self::weightToGrams($qty, $unit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return self::weightToGrams($amount * $qty, self::productStorageWeightUnit($product));
|
2026-06-09 11:17:07 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function isWeightBasedProductUnit(?string $unit): bool
|
|
|
|
|
{
|
|
|
|
|
$unit = trim((string) $unit);
|
|
|
|
|
|
|
|
|
|
return in_array($unit, ['کیلوگرم', 'گرم'], true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 21:37:05 +03:30
|
|
|
public static function meltedGoldBuyTitle(): string
|
|
|
|
|
{
|
|
|
|
|
return 'خرید ' . self::meltedGoldLabel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function meltedGoldSellTitle(): string
|
|
|
|
|
{
|
|
|
|
|
return 'فروش ' . self::meltedGoldLabel();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-18 16:38:46 +03:30
|
|
|
/**
|
|
|
|
|
* شرح معامله از دید فروشگاه برای حسابداری (پرگار و …).
|
|
|
|
|
* direction اپ: 1 = خرید مشتری → فروش به مشتری | 2 = فروش مشتری → خرید از مشتری
|
|
|
|
|
* اپ مشتری همچنان از productBuyTitle / productSellTitle استفاده میکند.
|
|
|
|
|
*/
|
|
|
|
|
public static function accountingShopTradeDesc(int $customerDirection, bool $fromCart = false): string
|
|
|
|
|
{
|
|
|
|
|
if ($fromCart) {
|
|
|
|
|
return 'فروش سبد به مشتری';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($customerDirection === 1) {
|
|
|
|
|
return 'فروش به مشتری';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($customerDirection === 2) {
|
|
|
|
|
return 'خرید از مشتری';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'معامله';
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 21:37:05 +03:30
|
|
|
public static function meltedGoldDepositTitle(): string
|
|
|
|
|
{
|
|
|
|
|
return 'موجودی ' . self::meltedGoldLabel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function meltedGoldWithdrawTitle(): string
|
|
|
|
|
{
|
|
|
|
|
return 'برداشت ' . self::meltedGoldLabel();
|
2026-06-03 20:24:07 +03:30
|
|
|
}
|
|
|
|
|
|
2026-06-03 21:52:37 +03:30
|
|
|
public static function isCountBasedProduct(?array $product): bool
|
|
|
|
|
{
|
|
|
|
|
if (!$product) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 17:29:18 +03:30
|
|
|
// معامله مقداری → نه تعدادی
|
2026-06-23 14:04:27 +03:30
|
|
|
if (self::usesQuantityTrade($product)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-16 17:29:18 +03:30
|
|
|
// آبشده/کاتد همیشه وزنی است
|
|
|
|
|
if (self::isMeltedTradeGroup($product['group_type'] ?? null)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// trade_by_quantity صراحتاً false/0 → تعدادی
|
|
|
|
|
// (حتی اگر unit=گرم فقط وزن هر عدد باشد)
|
|
|
|
|
if (array_key_exists('trade_by_quantity', $product) && $product['trade_by_quantity'] !== null) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// محصولات قدیمی بدون فلگ: واحد وزنی → مقداری
|
2026-06-09 11:17:07 +03:30
|
|
|
if (self::isWeightBasedProductUnit($product['unit'] ?? null)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 21:52:37 +03:30
|
|
|
if (isset($product['direct_price']) && (int) $product['direct_price'] !== 0) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$group = trim((string) ($product['group_type'] ?? ''));
|
2026-06-09 12:43:41 +03:30
|
|
|
if (in_array($group, ['مفتول مس'], true)) {
|
2026-06-03 21:52:37 +03:30
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return in_array($group, ['سکه', 'گوی', 'مکعب', 'شمش', 'سکه پارسیان'], true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 21:29:25 +03:30
|
|
|
public static function productUnitPayload($product): array
|
|
|
|
|
{
|
|
|
|
|
if (!$product) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (is_array($product)) {
|
|
|
|
|
return $product;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'unit' => $product->unit ?? null,
|
|
|
|
|
'direct_price' => $product->direct_price ?? null,
|
|
|
|
|
'group_type' => $product->group_type ?? null,
|
|
|
|
|
'amount' => $product->amount ?? null,
|
|
|
|
|
'trade_by_quantity' => $product->trade_by_quantity ?? null,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function convertWeight(float $value, ?string $fromUnit, ?string $toUnit): float
|
|
|
|
|
{
|
|
|
|
|
$from = trim((string) ($fromUnit ?? ''));
|
|
|
|
|
$to = trim((string) ($toUnit ?? ''));
|
|
|
|
|
|
|
|
|
|
if ($from === '' || $to === '' || $from === $to) {
|
|
|
|
|
return $value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$grams = self::weightToGrams($value, $from);
|
|
|
|
|
|
|
|
|
|
return $to === 'کیلوگرم' ? $grams / 1000 : $grams;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* مقدار و واحد نمایشی تراکنش — منبع حقیقت: unit محصول در ادمین.
|
|
|
|
|
* معامله مقداری + وزن دارد: تعداد ذخیرهشده × وزن هر واحد.
|
|
|
|
|
*/
|
|
|
|
|
public static function resolveTransactionDisplayGoldAndUnit(?array $payload, float $gold, bool $onMainWallet = false): array
|
|
|
|
|
{
|
|
|
|
|
if ($payload === []) {
|
|
|
|
|
return [$gold, self::tradeWeightUnit()];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$sign = $gold < 0 ? -1 : 1;
|
|
|
|
|
$absGold = abs($gold);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
self::usesQuantityTrade($payload)
|
|
|
|
|
&& self::hasProductUnitWeight($payload)
|
|
|
|
|
&& self::isWeightBasedProductUnit($payload['unit'] ?? null)
|
|
|
|
|
) {
|
|
|
|
|
$factor = (float) ($payload['amount'] ?? 1);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
round($absGold * $factor, 3) * $sign,
|
|
|
|
|
trim((string) $payload['unit']),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$unit = self::productQuantityUnit($payload);
|
|
|
|
|
$productWeightUnit = self::isWeightBasedProductUnit($payload['unit'] ?? null)
|
|
|
|
|
? trim((string) $payload['unit'])
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
if ($onMainWallet && $productWeightUnit) {
|
|
|
|
|
$tradeUnit = self::tradeWeightUnit();
|
|
|
|
|
|
|
|
|
|
if ($productWeightUnit !== $tradeUnit) {
|
|
|
|
|
return [
|
|
|
|
|
round(self::convertWeight($absGold, $tradeUnit, $productWeightUnit), 3) * $sign,
|
|
|
|
|
$productWeightUnit,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [$gold, $productWeightUnit];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [$gold, $unit];
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 21:52:37 +03:30
|
|
|
public static function productQuantityUnit(?array $product): string
|
|
|
|
|
{
|
2026-07-16 17:29:18 +03:30
|
|
|
if (self::isCountBasedProduct($product)) {
|
|
|
|
|
return 'عدد';
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 11:17:07 +03:30
|
|
|
$unit = trim((string) ($product['unit'] ?? ''));
|
|
|
|
|
if ($unit !== '') {
|
|
|
|
|
return $unit;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 21:29:25 +03:30
|
|
|
return self::tradeWeightUnit();
|
2026-06-03 21:52:37 +03:30
|
|
|
}
|
|
|
|
|
|
2026-06-17 14:05:44 +03:30
|
|
|
/** مطابق ادمین: وزن «دارد» => amount > 0 ؛ «ندارد» => amount = 0 */
|
|
|
|
|
public static function isMeltedTradeGroup(?string $groupType): bool
|
|
|
|
|
{
|
|
|
|
|
$group = trim((string) $groupType);
|
|
|
|
|
|
|
|
|
|
return in_array($group, ['آبشده', 'کاتد مس'], true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 21:52:37 +03:30
|
|
|
public static function hasProductUnitWeight(?array $product): bool
|
|
|
|
|
{
|
|
|
|
|
if (!$product || !isset($product['amount'])) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 14:05:44 +03:30
|
|
|
if (self::isMeltedTradeGroup($product['group_type'] ?? null)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-06-03 21:52:37 +03:30
|
|
|
|
2026-06-17 14:05:44 +03:30
|
|
|
return (float) $product['amount'] > 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 16:13:18 +03:30
|
|
|
/** معامله مقداری: فقط فیلد trade_by_quantity */
|
|
|
|
|
public static function usesQuantityTrade($product): bool
|
|
|
|
|
{
|
|
|
|
|
$value = is_array($product)
|
|
|
|
|
? ($product['trade_by_quantity'] ?? null)
|
|
|
|
|
: (is_object($product) ? ($product->trade_by_quantity ?? null) : null);
|
|
|
|
|
|
|
|
|
|
return !empty($value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-27 18:06:02 +03:30
|
|
|
/** ضریب قیمت محصول — فقط متغیر بدون مرجع اتوماتیک: قیمت هر گرم × وزن */
|
2026-06-17 14:05:44 +03:30
|
|
|
public static function productPriceMultiplier($product): float
|
|
|
|
|
{
|
|
|
|
|
$payload = is_array($product) ? $product : [
|
|
|
|
|
'amount' => is_object($product) ? ($product->amount ?? null) : null,
|
|
|
|
|
'unit' => is_object($product) ? ($product->unit ?? null) : null,
|
|
|
|
|
'direct_price' => is_object($product) ? ($product->direct_price ?? null) : null,
|
|
|
|
|
'group_type' => is_object($product) ? ($product->group_type ?? null) : null,
|
2026-06-27 18:06:02 +03:30
|
|
|
'reading_method' => is_object($product) ? ($product->reading_method ?? null) : null,
|
2026-06-17 14:05:44 +03:30
|
|
|
];
|
|
|
|
|
|
2026-06-27 18:06:02 +03:30
|
|
|
if (! empty($payload['reading_method'])) {
|
|
|
|
|
return 1.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (self::isCountBasedProduct($payload)) {
|
|
|
|
|
return 1.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((int) ($payload['direct_price'] ?? 0) !== 0) {
|
|
|
|
|
return 1.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! self::hasProductUnitWeight($payload)) {
|
2026-06-17 14:05:44 +03:30
|
|
|
return 1.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$amount = (float) ($payload['amount'] ?? 0);
|
|
|
|
|
|
|
|
|
|
return $amount > 0 ? $amount : 1.0;
|
2026-06-03 21:52:37 +03:30
|
|
|
}
|
|
|
|
|
|
2026-06-04 12:18:53 +03:30
|
|
|
public function jibitNcode($nationalCode, $mobile)
|
|
|
|
|
{
|
|
|
|
|
$counter = Cnt::where('key', 'jibit_counter')->first();
|
|
|
|
|
if (empty($counter)) {
|
|
|
|
|
$counter = new Cnt();
|
|
|
|
|
$counter->key = 'jibit_counter';
|
|
|
|
|
$counter->value = 0;
|
|
|
|
|
$counter->save();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 20:18:59 +03:30
|
|
|
if (!Storage::exists('accessTokenGibit') || !Storage::exists('refreshTokenGibit')) {
|
|
|
|
|
Storage::put('accessTokenGibit', 'توکن اولیه');
|
|
|
|
|
Storage::put('refreshTokenGibit', 'توکن رفرش اولیه');
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 12:18:53 +03:30
|
|
|
$apiData['refresh'] = Http::withoutVerifying()->withToken(Storage::get('refreshTokenGibit'))
|
|
|
|
|
->post('https://napi.jibit.ir/ide/v1/tokens/generate', [
|
|
|
|
|
'apiKey' => 'J5WxrPxPOg',
|
|
|
|
|
'secretKey' => '7dgeE70xQXsQL7r36j3wif6X9',
|
|
|
|
|
])->json();
|
2026-07-02 20:18:59 +03:30
|
|
|
Storage::put('accessTokenGibit', $apiData['refresh']['accessToken'] ?? '');
|
|
|
|
|
if (!empty($apiData['refresh']['refreshToken'])) {
|
|
|
|
|
Storage::put('refreshTokenGibit', $apiData['refresh']['refreshToken']);
|
|
|
|
|
}
|
2026-06-04 12:18:53 +03:30
|
|
|
|
|
|
|
|
$response = Http::withoutVerifying()->withToken(Storage::get('accessTokenGibit'))
|
|
|
|
|
->get("https://napi.jibit.ir/ide/v1/services/matching?mobileNumber=$mobile&nationalCode=$nationalCode")
|
|
|
|
|
->json();
|
|
|
|
|
|
|
|
|
|
if ((isset($response['code'])) && $response['code'] == 'forbidden') {
|
|
|
|
|
$refreshToken = Http::withoutVerifying()->withToken(Storage::get('refreshTokenGibit'))
|
|
|
|
|
->post('https://napi.jibit.ir/ide/v1/tokens/generate', [
|
|
|
|
|
'apiKey' => 'J5WxrPxPOg',
|
|
|
|
|
'secretKey' => '7dgeE70xQXsQL7r36j3wif6X9',
|
|
|
|
|
])->json();
|
2026-07-02 20:18:59 +03:30
|
|
|
Storage::put('accessTokenGibit', $refreshToken['accessToken'] ?? '');
|
|
|
|
|
if (!empty($refreshToken['refreshToken'])) {
|
|
|
|
|
Storage::put('refreshTokenGibit', $refreshToken['refreshToken']);
|
|
|
|
|
}
|
2026-06-04 12:18:53 +03:30
|
|
|
$response = Http::withoutVerifying()->withToken(Storage::get('accessTokenGibit'))
|
|
|
|
|
->get("https://napi.jibit.ir/ide/v1/services/matching?mobileNumber=$mobile&nationalCode=$nationalCode")
|
|
|
|
|
->json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$counter->value = (int) $counter->value + 1;
|
|
|
|
|
$counter->save();
|
|
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 17:19:25 +03:30
|
|
|
public static function jibit($method, $p1 = null, $p2 = null, $p3 = null)
|
|
|
|
|
{
|
2026-04-26 20:09:21 +00:00
|
|
|
$counter = Cnt::where('key', 'jibit_counter')->first();
|
|
|
|
|
if(empty($counter)){
|
|
|
|
|
$counter = new Cnt();
|
|
|
|
|
$counter->key = "jibit_counter";
|
|
|
|
|
$counter->value = 0;
|
|
|
|
|
$counter->save();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 17:19:25 +03:30
|
|
|
$apiData['refresh'] = Http::withoutVerifying()->withoutVerifying()->withToken(Storage::get('refreshTokenGibit'))
|
|
|
|
|
->post("https://napi.jibit.ir/ide/v1/tokens/generate", ['apiKey' => "J5WxrPxPOg", 'secretKey' => "7dgeE70xQXsQL7r36j3wif6X9",])->json();
|
|
|
|
|
Storage::put('accessTokenGibit', $apiData['refresh']['accessToken']);
|
|
|
|
|
|
|
|
|
|
if (!Storage::exists('accessTokenGibit') || !Storage::exists('refreshTokenGibit')) {
|
|
|
|
|
Storage::put('accessTokenGibit', 'توکن اولیه');
|
|
|
|
|
Storage::put('refreshTokenGibit', 'توکن رفرش اولیه');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$apiData['cartMatching'] = Http::withoutVerifying()->withToken(Storage::get('accessTokenGibit'))->get("https://napi.jibit.ir/ide/v1/services/matching?cardNumber=$p1&nationalCode=$p2&birthDate=$p3")->json();
|
|
|
|
|
if ((isset($apiData['cartMatching']['code'])) and $apiData['cartMatching']['code'] == "forbidden") {
|
|
|
|
|
$apiData['refresh'] = Http::withoutVerifying()->withToken(Storage::get('refreshTokenGibit'))
|
|
|
|
|
->post("https://napi.jibit.ir/ide/v1/tokens/generate", ['apiKey' => "J5WxrPxPOg", 'secretKey' => "7dgeE70xQXsQL7r36j3wif6X9",])->json();
|
|
|
|
|
Storage::put('accessTokenGibit', $apiData['refresh']['accessToken']);
|
|
|
|
|
Storage::put('refreshTokenGibit', $apiData['refresh']['refreshToken']);
|
|
|
|
|
$apiData['cartMatching'] = Http::withoutVerifying()->withToken(Storage::get('accessTokenGibit'))->get("https://napi.jibit.ir/ide/v1/services/matching?cardNumber=$p1&nationalCode=$p2&birthDate=$p3")->json();
|
|
|
|
|
}
|
2026-04-26 20:09:21 +00:00
|
|
|
//update jibit counter
|
|
|
|
|
$counter->value = (int)$counter->value + 1;
|
|
|
|
|
$counter->save();
|
|
|
|
|
|
2026-04-19 17:19:25 +03:30
|
|
|
if (isset( $apiData['cartMatching']['matched']) and $apiData['cartMatching']['matched'] == true) {
|
|
|
|
|
$apiData['cartData'] = Http::withoutVerifying()->withToken(Storage::get('accessTokenGibit'))->get("https://napi.jibit.ir/ide/v1/cards?number=$p1&iban=true")->json();
|
|
|
|
|
return $apiData;
|
2026-04-28 16:37:12 +00:00
|
|
|
}elseif(isset( $apiData['cartMatching']['matched']) and $apiData['cartMatching']['matched'] == false){
|
|
|
|
|
return $apiData;
|
2026-04-19 17:19:25 +03:30
|
|
|
} else {
|
|
|
|
|
throw new UnprocessableEntityHttpException($apiData['cartMatching']['message']);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 14:03:11 +00:00
|
|
|
}
|