Merge origin/Abshode_Khanegi and resolve WalletController conflict.
Keep unified charge API from remote while preserving main-wallet metal detection for deposits and transaction labels via BalanceReportService. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -16,6 +16,7 @@ use Rooyesh\Wallet\interfaces\WalletInterface;
|
||||
use Rooyesh\Wallet\models\Transaction;
|
||||
use Rooyesh\Wallet\models\Wallet;
|
||||
use App\Models\Setting;
|
||||
use App\Services\BalanceReportService;
|
||||
use App\Services\ConfigService;
|
||||
use App\Services\WalletBalanceService;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
@@ -133,19 +134,27 @@ class CustomerController extends SuperController
|
||||
return response()->json(true);
|
||||
}
|
||||
public function tableWallets(Request $request){
|
||||
$query_table = Customer::with(['wallet','referral'])->where("is_archive",false)/*->where('type',1)*/;
|
||||
$query_table = Customer::with(['wallet.product:id,metal_id', 'referral'])->where("is_archive",false)/*->where('type',1)*/;
|
||||
TableEngine::create($query_table, $request)
|
||||
->initSearch($this->search)
|
||||
->select(['*',Customer::$SELECT_TYPE_STR])
|
||||
->export($query_table);
|
||||
|
||||
$balanceService = app(WalletBalanceService::class);
|
||||
$mainWalletProductIds = app(ConfigService::class)->mainWalletProductIds();
|
||||
$balanceReport = app(BalanceReportService::class);
|
||||
$catalogMetalIds = $balanceService->catalogMetalIds();
|
||||
$query_table['catalogMetals'] = $balanceReport->catalogMetalsMeta();
|
||||
|
||||
if (isset($query_table['rows'])) {
|
||||
$query_table['rows'] = collect($query_table['rows'])->map(function ($row) use ($balanceService, $mainWalletProductIds) {
|
||||
$query_table['rows'] = collect($query_table['rows'])->map(function ($row) use ($balanceService, $catalogMetalIds) {
|
||||
$wallets = collect($row['wallet'] ?? []);
|
||||
$row['wallet_cash'] = $balanceService->mainWalletCash($wallets);
|
||||
$row['wallet_gold'] = $balanceService->totalGoldWeight($wallets, $mainWalletProductIds);
|
||||
$metalMap = $balanceService->buildCustomerWalletMetalDisplayMap($wallets, $catalogMetalIds);
|
||||
$row['wallet_metal_gold'] = $metalMap;
|
||||
foreach ($catalogMetalIds as $metalId) {
|
||||
$row['wallet_metal_gold_' . $metalId] = $metalMap[(string) $metalId] ?? 0.0;
|
||||
}
|
||||
$row['wallet_gold'] = array_sum($metalMap);
|
||||
|
||||
return $row;
|
||||
})->values();
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\Price;
|
||||
use App\Models\Product;
|
||||
use App\Models\TransactionsAdmin;
|
||||
use App\Models\Withdraw;
|
||||
use App\Services\BalanceReportService;
|
||||
use App\Services\ConfigService;
|
||||
use Carbon\Carbon;
|
||||
use Hekmatinasser\Verta\Verta;
|
||||
@@ -149,7 +150,7 @@ class DashboardController extends Controller
|
||||
return $cnt;
|
||||
}
|
||||
|
||||
public function getBalance(Request $request)
|
||||
public function getBalance(Request $request, BalanceReportService $balanceReportService)
|
||||
{
|
||||
$startDate = Carbon::parse($request->startDate)->startOfDay();
|
||||
$endDate = Carbon::parse($request->endDate)->endOfDay();
|
||||
@@ -158,6 +159,12 @@ class DashboardController extends Controller
|
||||
$response["kalhor"]=[];
|
||||
|
||||
$excludeCustomerId = (int) app(ConfigService::class)->getInt('balance_report_exclude_customer_id', 4);
|
||||
$catalogMetalIds = $balanceReportService->catalogMetalIds();
|
||||
if ($catalogMetalIds === []) {
|
||||
$catalogMetalIds = [1];
|
||||
}
|
||||
$limanMetalBuckets = $balanceReportService->initMetalBalanceBuckets($catalogMetalIds);
|
||||
$kalhorMetalBuckets = $balanceReportService->initMetalBalanceBuckets($catalogMetalIds);
|
||||
|
||||
$response["pay_at_update"] = Order::whereNull('pay_at')->update(['pay_at' => DB::raw('created_at')]);
|
||||
//liman
|
||||
@@ -171,6 +178,8 @@ class DashboardController extends Controller
|
||||
$response["liman"]["balance_abs"]["gold"] = 0;
|
||||
$response["liman"]["balance_abs"]["cash"] = 0;
|
||||
foreach ($response["liman"]["orders"] as $OrderGoldBalanceRow) {
|
||||
$balanceReportService->applyOrderToBuckets($limanMetalBuckets, $OrderGoldBalanceRow);
|
||||
|
||||
if ($OrderGoldBalanceRow->direction == 1){
|
||||
$response["liman"]["balance"]["gold"] += $OrderGoldBalanceRow->amount;
|
||||
$response["liman"]["balance"]["cash"] -= $OrderGoldBalanceRow->total_price;
|
||||
@@ -212,10 +221,22 @@ class DashboardController extends Controller
|
||||
$response["kalhor"]["balance"]['gold'] += $data['Count'];
|
||||
$response["kalhor"]["balance"]['cash'] -= $data['TotalPrice'];
|
||||
$direction =2;
|
||||
$balanceReportService->applyKalhorSlice(
|
||||
$kalhorMetalBuckets,
|
||||
(float) $data['Count'],
|
||||
(float) $data['TotalPrice'],
|
||||
2
|
||||
);
|
||||
} elseif ($data['DealTypeStr'] == "فروش" or $data['DealTypeStr'] == "فروش _ دستی") {
|
||||
$response["kalhor"]["balance"]['gold'] -= $data['Count'];
|
||||
$response["kalhor"]["balance"]['cash'] += $data['TotalPrice'];
|
||||
$direction =1;
|
||||
$balanceReportService->applyKalhorSlice(
|
||||
$kalhorMetalBuckets,
|
||||
(float) $data['Count'],
|
||||
(float) $data['TotalPrice'],
|
||||
1
|
||||
);
|
||||
}
|
||||
$response["kalhor"]["balance_abs"]['gold'] += $data['Count'];
|
||||
$response["kalhor"]["balance_abs"]['cash'] += $data['TotalPrice'];
|
||||
@@ -244,6 +265,12 @@ class DashboardController extends Controller
|
||||
//main
|
||||
$response['cashBalance']= -$response["liman"]["balance"]["cash"]+$response["kalhor"]["balance"]["cash"];
|
||||
$response['goldBalance']= -$response["liman"]["balance"]["gold"]+$response["kalhor"]["balance"]["gold"];
|
||||
|
||||
$metalBalances = $balanceReportService->buildMetalBalancesResponse($limanMetalBuckets, $kalhorMetalBuckets);
|
||||
$response['metalBalances'] = $metalBalances['items'];
|
||||
if (!empty($metalBalances['items'])) {
|
||||
$response['goldBalance'] = $metalBalances['totalGoldBalance'];
|
||||
}
|
||||
//mazaneh
|
||||
$changeMesghal = config('change_mesghal');
|
||||
|
||||
|
||||
@@ -14,16 +14,20 @@ class FileController extends Controller
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function createDir()
|
||||
protected function createDir(): string
|
||||
{
|
||||
$now = Verta();
|
||||
$path = preg_replace('/-/', '/', $now->formatDate());
|
||||
if (!Storage::exists($path)) {
|
||||
Storage::makeDirectory($path);
|
||||
Storage::makeDirectory($path . '/' . 'thumb');
|
||||
$disk = Storage::disk('public');
|
||||
|
||||
if (! $disk->exists($path)) {
|
||||
$disk->makeDirectory($path);
|
||||
$disk->makeDirectory($path.'/thumb');
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
// Store FIle in Public Storage By Date
|
||||
@@ -31,17 +35,17 @@ class FileController extends Controller
|
||||
|
||||
$data = $request->all();
|
||||
$path = $this->createDir();
|
||||
$data['original_name'] = $request->file('file')->getClientOriginalName();
|
||||
$data['ext'] = $request->file('file')->getClientOriginalExtension();
|
||||
$uploaded = $request->file('file');
|
||||
$data['original_name'] = $uploaded->getClientOriginalName();
|
||||
$data['ext'] = $uploaded->getClientOriginalExtension();
|
||||
if (!isset($data['title']) || is_null($data['title'])) {
|
||||
$data['title'] = $data['original_name'];
|
||||
}
|
||||
$file_name_src = md5(time() . $data['original_name']);
|
||||
$file_name = $file_name_src . '.' . $data['ext'];
|
||||
$file = $request->file('file');
|
||||
$data['has_thumb'] = false;
|
||||
$data['size'] = $file->getSize();
|
||||
$file->move(public_path('storage/' . $path . '/'), $file_name);
|
||||
$data['size'] = $uploaded->getSize();
|
||||
Storage::disk('public')->putFileAs($path, $uploaded, $file_name);
|
||||
$data['src'] = 'storage/' . $path . '/' . $file_name;
|
||||
// $data['size'] = $file->getSize();
|
||||
$data['user_id'] = auth()->id();
|
||||
|
||||
@@ -5,26 +5,61 @@ namespace App\Http\Controllers\Admin;
|
||||
use App\DB\Te\TableEngine;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Order;
|
||||
use App\Services\BalanceReportService;
|
||||
use App\Services\WalletBalanceService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class OrderController extends Controller
|
||||
{
|
||||
public function table(Request $request){
|
||||
$records = Order::with(['customer','transactions']);
|
||||
$balanceReport = app(BalanceReportService::class);
|
||||
$catalogMetalIds = app(WalletBalanceService::class)->catalogMetalIds();
|
||||
|
||||
$records = Order::with([
|
||||
'customer',
|
||||
'transactions.product:id,title,unit,metal_id,group_type,amount,trade_by_quantity,direct_price',
|
||||
]);
|
||||
TableEngine::create($records, $request->all())
|
||||
->initSearch(['customer.name'])
|
||||
->select([
|
||||
'*',Order::$SELECT_STS_STR
|
||||
])->export($records);
|
||||
|
||||
if (isset($records['rows'])) {
|
||||
foreach ($records['rows'] as &$row) {
|
||||
$balanceReport->enrichOrderTableRow($row, $catalogMetalIds);
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
|
||||
$records['catalogMetals'] = $balanceReport->catalogMetalsMeta();
|
||||
|
||||
return response()->json($records);
|
||||
}
|
||||
|
||||
public function singleOrderTable(Request $request){
|
||||
$records = Order::with(['customer'])->where('type',2);
|
||||
$balanceReport = app(BalanceReportService::class);
|
||||
$catalogMetalIds = app(WalletBalanceService::class)->catalogMetalIds();
|
||||
|
||||
$records = Order::with([
|
||||
'customer',
|
||||
'transactions.product:id,title,unit,metal_id,group_type,amount,trade_by_quantity,direct_price',
|
||||
])->where('type',2);
|
||||
TableEngine::create($records, $request->all())
|
||||
->initSearch(['customer.name'])
|
||||
->select([
|
||||
'*',Order::$SELECT_STS_STR
|
||||
])->export($records);
|
||||
|
||||
if (isset($records['rows'])) {
|
||||
foreach ($records['rows'] as &$row) {
|
||||
$balanceReport->enrichOrderTableRow($row, $catalogMetalIds);
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
|
||||
$records['catalogMetals'] = $balanceReport->catalogMetalsMeta();
|
||||
|
||||
return response()->json($records);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,6 +612,10 @@ class ProductController extends SuperController
|
||||
unset($data['direct_price']);
|
||||
}
|
||||
|
||||
if (!Product::hasInventoryColumn()) {
|
||||
unset($data['inventory']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,9 @@ use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
use App\Http\Controllers\Admin\TahesabController;
|
||||
use App\Models\Setting;
|
||||
use App\Models\User;
|
||||
use App\Services\BalanceReportService;
|
||||
use App\Services\ConfigService;
|
||||
use App\Services\TransactionMetalService;
|
||||
use App\Services\WalletBalanceService;
|
||||
use Hekmatinasser\Verta\Verta;
|
||||
|
||||
|
||||
@@ -221,10 +222,172 @@ class WalletController extends Controller
|
||||
return response()->json(true);
|
||||
}
|
||||
|
||||
public function index(Request $request, TransactionMetalService $transactionMetalService)
|
||||
/** گزینههای شارژ کیف پول — نقدی + محصولات فعال */
|
||||
public function chargeTargets()
|
||||
{
|
||||
$records = Transaction::with(['customer', 'wallet.product:id,metal_id', 'product:id,metal_id'])
|
||||
->whereNull('fund_id');
|
||||
$configService = app(ConfigService::class);
|
||||
$mainWalletProductIds = $configService->mainWalletProductIds();
|
||||
$balanceReport = app(BalanceReportService::class);
|
||||
|
||||
$targets = [
|
||||
[
|
||||
'key' => 'money',
|
||||
'kind' => 'money',
|
||||
'label' => 'نقدی (ریال)',
|
||||
'unit' => 'ریال',
|
||||
],
|
||||
];
|
||||
|
||||
$products = Product::query()
|
||||
->where('active', 1)
|
||||
->orderBy('metal_id')
|
||||
->orderBy('id')
|
||||
->get(['id', 'title', 'unit', 'metal_id', 'group_type', 'trade_by_quantity', 'amount', 'direct_price']);
|
||||
|
||||
foreach ($products as $product) {
|
||||
$usesMainWallet = in_array((int) $product->id, $mainWalletProductIds, true);
|
||||
$label = (string) $product->title;
|
||||
if ($usesMainWallet) {
|
||||
$label .= ' (کیف اصلی)';
|
||||
}
|
||||
|
||||
$targets[] = [
|
||||
'key' => 'product:' . $product->id,
|
||||
'kind' => 'metal',
|
||||
'product_id' => (int) $product->id,
|
||||
'label' => $label,
|
||||
'unit' => $this->resolveProductChargeUnit($product),
|
||||
'metal_id' => max(1, (int) ($product->metal_id ?? 1)),
|
||||
'uses_main_wallet' => $usesMainWallet,
|
||||
'trade_by_quantity' => (bool) $product->trade_by_quantity,
|
||||
'group_type' => (string) ($product->group_type ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'targets' => $targets,
|
||||
'catalogMetals' => $balanceReport->catalogMetalsMeta(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* شارژ یکپارچه کیف پول — نقدی یا محصول (با رعایت کیف اصلی برای آبشده).
|
||||
*/
|
||||
public function charge_wallet(Request $request)
|
||||
{
|
||||
$productId = (int) $request->input('product_id', 0);
|
||||
if ($productId > 0) {
|
||||
return $this->chargeProductMetal($request, $productId);
|
||||
}
|
||||
|
||||
return $this->charge($request);
|
||||
}
|
||||
|
||||
private function chargeProductMetal(Request $request, int $productId)
|
||||
{
|
||||
$data = $request->all();
|
||||
$product = Product::findOrFail($productId);
|
||||
$customer = Customer::findOrFail($data['customer_id']);
|
||||
$deposit = ((float) ($data['value'] ?? 0)) > 0;
|
||||
|
||||
$effectiveProductId = Controller::effectiveProductWalletId($productId);
|
||||
$wallet_interface = new WalletInterface(
|
||||
(int) $data['customer_id'],
|
||||
(int) $customer->type,
|
||||
null,
|
||||
$effectiveProductId
|
||||
);
|
||||
$wallet = $wallet_interface->wallet;
|
||||
|
||||
if (!$deposit && $wallet !== null && (float) $wallet->gold < abs((float) $data['value'])) {
|
||||
throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست');
|
||||
}
|
||||
|
||||
$goldBlock = 0;
|
||||
$user = auth()->user();
|
||||
$unitLabel = $this->resolveProductChargeUnit($product);
|
||||
|
||||
$Withdraw = Withdraw::create([
|
||||
'user_id' => $user->id,
|
||||
'customer_id' => $customer->id,
|
||||
'gold' => $data['value'],
|
||||
'note' => $data['note'] ?? null,
|
||||
'sts' => 4,
|
||||
]);
|
||||
|
||||
new TransactionInterface($wallet_interface, [
|
||||
'cash' => 0,
|
||||
'gold' => $data['value'],
|
||||
'product_id' => $productId,
|
||||
'expire_at' => now(),
|
||||
'note' => $data['note'] ?? null,
|
||||
'withdraw_id' => $Withdraw->id,
|
||||
'active' => true,
|
||||
], 7, $deposit, false, $goldBlock);
|
||||
|
||||
if ($this->Config('send_to_tahesab') == 'true') {
|
||||
try {
|
||||
$data['direction'] = $deposit ? 1 : 0;
|
||||
$tahesab = new TahesabController();
|
||||
$tahesab->walletGold($data);
|
||||
} catch (\Exception $e) {
|
||||
return response($e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
$status = $deposit ? ' شارژ شد ' : ' کسر شد ';
|
||||
$formatted = self::formatSmsGoldWeight($data['value']);
|
||||
|
||||
if ($this->Config('charge_notif') == 'true') {
|
||||
$message = 'کیف پول کاربر ' . $customer->name . ' به مقدار ' . $formatted . ' ' . $unitLabel . ' در تاریخ ' . verta()->format('Y/n/j H:i:s') . $status . 'بابت' . "\n"
|
||||
. ($data['note'] ?? '')
|
||||
. "\n"
|
||||
. $this->Config('name_fa');
|
||||
User::where('send_sms', 1)->get()->each(function ($admin) use ($message) {
|
||||
$this->send($admin->mobile, $message);
|
||||
});
|
||||
}
|
||||
|
||||
$message = 'کیف پول شما به اندازه ' . $formatted . ' ' . $unitLabel . ' در تاریخ ' . verta()->format('Y/n/j H:i:s') . $status . ' بابت' . "\n"
|
||||
. ($data['note'] ?? '')
|
||||
. "\n"
|
||||
. $this->Config('name_fa');
|
||||
$this->Send($customer->mobile, $message);
|
||||
|
||||
return response()->json(true);
|
||||
}
|
||||
|
||||
private function resolveProductChargeUnit(Product $product): string
|
||||
{
|
||||
$payload = Controller::productUnitPayload($product);
|
||||
|
||||
if (
|
||||
Controller::usesQuantityTrade($payload)
|
||||
&& !Controller::isWeightBasedProductUnit($payload['unit'] ?? null)
|
||||
) {
|
||||
$unit = trim((string) ($payload['unit'] ?? ''));
|
||||
|
||||
return $unit !== '' ? $unit : 'عدد';
|
||||
}
|
||||
|
||||
if (Controller::isWeightBasedProductUnit($payload['unit'] ?? null)) {
|
||||
return trim((string) $payload['unit']);
|
||||
}
|
||||
|
||||
return Controller::productStorageWeightUnit($product);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$balanceReport = app(BalanceReportService::class);
|
||||
$balanceService = app(WalletBalanceService::class);
|
||||
$catalogMetalIds = $balanceService->catalogMetalIds();
|
||||
|
||||
$records = Transaction::with([
|
||||
'customer',
|
||||
'wallet.product:id,metal_id',
|
||||
'product:id,title,metal_id,group_type,unit,amount,trade_by_quantity,direct_price',
|
||||
])->whereNull('fund_id');
|
||||
TableEngine::create($records, $request->all())
|
||||
->initSearch(['order_id', 'customer.name'])
|
||||
->select([
|
||||
@@ -232,9 +395,13 @@ class WalletController extends Controller
|
||||
])->export($records);
|
||||
|
||||
if (isset($records['rows'])) {
|
||||
$records['rows'] = $transactionMetalService->enrichTransactionRows($records['rows']);
|
||||
foreach ($records['rows'] as &$row) {
|
||||
$balanceReport->enrichTransactionTableRow($row, $catalogMetalIds);
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
$records['catalogMetals'] = $transactionMetalService->catalogMetals();
|
||||
|
||||
$records['catalogMetals'] = $balanceReport->catalogMetalsMeta();
|
||||
|
||||
return response()->json($records);
|
||||
}
|
||||
@@ -289,28 +456,42 @@ class WalletController extends Controller
|
||||
public function indexWallet(Request $request)
|
||||
{
|
||||
$lastPrice = (float) (Price::latest()->first()?->price ?? 0);
|
||||
$balanceService = app(WalletBalanceService::class);
|
||||
$catalogMetalIds = $balanceService->catalogMetalIds();
|
||||
$mainWalletMetalId = $balanceService->resolveMainWalletMetalId();
|
||||
|
||||
// همان فیلتر/جستجوی جدول — بدون صفحهبندی — برای مجموع ردیفهای قابل مشاهده
|
||||
$sumQuery = Wallet::query();
|
||||
TableEngine::create($sumQuery, $request->all())
|
||||
->initSearch(['customer.name']);
|
||||
|
||||
$totalGold = (float) (clone $sumQuery)->sum('gold');
|
||||
$sumRows = (clone $sumQuery)->with('product:id,metal_id')->get();
|
||||
$metalBalances = $balanceService->buildWalletMetalSums($sumRows, $catalogMetalIds);
|
||||
$totalGold = array_sum(array_column($metalBalances, 'gold'));
|
||||
$totalGoldToCash = array_sum(array_column($metalBalances, 'goldToCash'));
|
||||
$totalCash = (float) (clone $sumQuery)->sum('cash');
|
||||
|
||||
$sums = [
|
||||
'gold' => round($totalGold, 2),
|
||||
'goldToCash' => round($totalGold * $lastPrice, 2),
|
||||
'goldToCash' => round($totalGoldToCash, 2),
|
||||
'cash' => round($totalCash, 2),
|
||||
'cashToGold' => $lastPrice > 0 ? round($totalCash / $lastPrice, 2) : 0,
|
||||
'metalBalances' => $metalBalances,
|
||||
];
|
||||
|
||||
$records = Wallet::with('customer');
|
||||
$records = Wallet::with(['customer', 'product:id,metal_id']);
|
||||
TableEngine::create($records, $request->all())
|
||||
->initSearch(['customer.name'])
|
||||
->select(['*'])
|
||||
->export($records);
|
||||
|
||||
if (isset($records['rows'])) {
|
||||
foreach ($records['rows'] as &$row) {
|
||||
$balanceService->enrichWalletTableRow($row, $catalogMetalIds, $mainWalletMetalId);
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
|
||||
$records['sums'] = $sums;
|
||||
|
||||
return response()->json($records);
|
||||
|
||||
@@ -73,6 +73,26 @@ class Controller extends BaseController
|
||||
return $formatted !== '' ? $formatted : '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* گزینههای 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];
|
||||
}
|
||||
|
||||
public static function Send($to , $msg, $is_customer = true)
|
||||
{
|
||||
$r = self::SendMessage($to , $msg, $is_customer);
|
||||
@@ -139,26 +159,32 @@ class Controller extends BaseController
|
||||
]);
|
||||
$platforms[] = "0098:{$to}";*/
|
||||
|
||||
$response = Http::timeout(15)
|
||||
->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'secret_key' => 'f21debfb-83ac-4624-a2f4-f08375043181',
|
||||
])
|
||||
->withOptions([
|
||||
'curl' => [
|
||||
\CURLOPT_IPRESOLVE => \CURL_IPRESOLVE_V4,
|
||||
]
|
||||
])
|
||||
->post('http://185.231.112.101/api/createsms', [
|
||||
try {
|
||||
$http = Http::timeout(15)
|
||||
->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'secret_key' => 'f21debfb-83ac-4624-a2f4-f08375043181',
|
||||
]);
|
||||
|
||||
$curlOptions = self::httpClientOptions();
|
||||
if ($curlOptions !== []) {
|
||||
$http = $http->withOptions($curlOptions);
|
||||
}
|
||||
|
||||
$response = $http->post('http://185.231.112.101/api/createsms', [
|
||||
'phone' => $to,
|
||||
'host' => $config->Config('name_fa') ?? gethostname(),
|
||||
'text' => trim($msg),
|
||||
'platform' => 'rayegansms',
|
||||
]);
|
||||
|
||||
$platforms[] = $response->successful()
|
||||
? "rayegansms:{$to}"
|
||||
: "rayegansms_failed:{$to}:{$response->status()}";
|
||||
$platforms[] = $response->successful()
|
||||
? "rayegansms:{$to}"
|
||||
: "rayegansms_failed:{$to}:{$response->status()}";
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
$platforms[] = "rayegansms_error:{$to}";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -435,6 +461,10 @@ class Controller extends BaseController
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self::usesQuantityTrade($product)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self::isWeightBasedProductUnit($product['unit'] ?? null)) {
|
||||
return false;
|
||||
}
|
||||
@@ -451,6 +481,86 @@ class Controller extends BaseController
|
||||
return in_array($group, ['سکه', 'گوی', 'مکعب', 'شمش', 'سکه پارسیان'], true);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
public static function productQuantityUnit(?array $product): string
|
||||
{
|
||||
$unit = trim((string) ($product['unit'] ?? ''));
|
||||
@@ -462,7 +572,7 @@ class Controller extends BaseController
|
||||
return 'عدد';
|
||||
}
|
||||
|
||||
return 'گرم';
|
||||
return self::tradeWeightUnit();
|
||||
}
|
||||
|
||||
/** مطابق ادمین: وزن «دارد» => amount > 0 ؛ «ندارد» => amount = 0 */
|
||||
|
||||
@@ -137,11 +137,7 @@ class WalletController extends Controller
|
||||
} else {
|
||||
$record->title = ($record->gold > 0 ? self::productBuyTitle($wallet->product->title) : self::productSellTitle($wallet->product->title));
|
||||
}
|
||||
$record->unit = self::productQuantityUnit([
|
||||
'unit' => $wallet->product->unit ?? null,
|
||||
'direct_price' => $wallet->product->direct_price ?? null,
|
||||
'group_type' => $wallet->product->group_type ?? null,
|
||||
]);
|
||||
$record->unit = self::productQuantityUnit(self::productUnitPayload($wallet->product));
|
||||
} else {
|
||||
$record->title = $this->resolveMainWalletTransactionTitle($record);
|
||||
}
|
||||
@@ -406,17 +402,12 @@ class WalletController extends Controller
|
||||
return;
|
||||
}
|
||||
|
||||
$productId = $this->resolveAbshodeProductId($record);
|
||||
if (!$productId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$productTitle = $record->product->title
|
||||
?? Product::query()->whereKey($productId)->value('title');
|
||||
if (!$productTitle) {
|
||||
$product = $this->resolveTransactionProduct($record);
|
||||
if (!$product || trim((string) $product->title) === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$productTitle = trim((string) $product->title);
|
||||
$direction = (int) ($record->order->direction ?? 0);
|
||||
if ($direction === 1 || ($direction === 0 && (float) ($record->gold ?? 0) > 0)) {
|
||||
$record->title = self::productBuyTitle($productTitle);
|
||||
@@ -434,9 +425,14 @@ class WalletController extends Controller
|
||||
return (int) $record->product_id;
|
||||
}
|
||||
|
||||
$note = (string) ($record->note ?? '');
|
||||
if ($note !== '' && preg_match('/(?:^|;)product_id:(\d+)/', $note, $matches)) {
|
||||
return (int) $matches[1];
|
||||
$noteProductId = $this->productIdFromTransactionNote($record->note ?? null);
|
||||
if ($noteProductId) {
|
||||
return $noteProductId;
|
||||
}
|
||||
|
||||
$wallet = $this->resolveTransactionWallet($record);
|
||||
if ($wallet && !empty($wallet->product_id)) {
|
||||
return (int) $wallet->product_id;
|
||||
}
|
||||
|
||||
if (empty($record->order_id)) {
|
||||
@@ -449,7 +445,33 @@ class WalletController extends Controller
|
||||
->where('product_id', '>', 0)
|
||||
->value('product_id');
|
||||
|
||||
return $siblingProductId ? (int) $siblingProductId : null;
|
||||
if ($siblingProductId) {
|
||||
return (int) $siblingProductId;
|
||||
}
|
||||
|
||||
$siblingNotes = Transaction::query()
|
||||
->where('order_id', $record->order_id)
|
||||
->whereNotNull('note')
|
||||
->pluck('note');
|
||||
|
||||
foreach ($siblingNotes as $note) {
|
||||
$productId = $this->productIdFromTransactionNote($note);
|
||||
if ($productId) {
|
||||
return $productId;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function productIdFromTransactionNote(?string $note): ?int
|
||||
{
|
||||
$note = (string) ($note ?? '');
|
||||
if ($note === '' || !preg_match('/(?:^|;)product_id:(\d+)/', $note, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
private function applyMetalTypeTransactionTitle($record): void
|
||||
@@ -462,7 +484,7 @@ class WalletController extends Controller
|
||||
return;
|
||||
}
|
||||
|
||||
$wallet = $record->wallet ?? null;
|
||||
$wallet = $this->resolveTransactionWallet($record);
|
||||
if ($wallet && !empty($wallet->product)) {
|
||||
return;
|
||||
}
|
||||
@@ -500,18 +522,12 @@ class WalletController extends Controller
|
||||
return;
|
||||
}
|
||||
|
||||
$productId = $this->resolveAbshodeProductId($record);
|
||||
if (!$productId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$productTitle = $record->product->title
|
||||
?? $record->wallet->product->title
|
||||
?? Product::query()->whereKey($productId)->value('title');
|
||||
if (!$productTitle) {
|
||||
$product = $this->resolveTransactionProduct($record);
|
||||
if (!$product || trim((string) $product->title) === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$productTitle = trim((string) $product->title);
|
||||
$expectedBuy = self::productBuyTitle($productTitle);
|
||||
$expectedSell = self::productSellTitle($productTitle);
|
||||
|
||||
@@ -527,11 +543,28 @@ class WalletController extends Controller
|
||||
|
||||
if ($direction === 2 || ($direction === 0 && (float) ($record->gold ?? 0) < 0)) {
|
||||
$record->title = $expectedSell;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::isGenericMetalTransactionTitle($record->title)) {
|
||||
$record->title = $expectedBuy;
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveMainWalletTransactionTitle($record): string
|
||||
{
|
||||
$product = $this->resolveTransactionProduct($record);
|
||||
if ($product && trim((string) $product->title) !== '') {
|
||||
$productTitle = trim((string) $product->title);
|
||||
if ((int) $record->type === 1) {
|
||||
return self::productBuyTitle($productTitle);
|
||||
}
|
||||
|
||||
if ((int) $record->type === 2) {
|
||||
return self::productSellTitle($productTitle);
|
||||
}
|
||||
}
|
||||
|
||||
if ((int) $record->type === 1) {
|
||||
return self::mainMetalBuyTitle();
|
||||
}
|
||||
@@ -576,35 +609,86 @@ class WalletController extends Controller
|
||||
}
|
||||
|
||||
if (!empty($record->wallet?->product)) {
|
||||
$record->unit = self::productQuantityUnit([
|
||||
'unit' => $record->wallet->product->unit ?? null,
|
||||
'direct_price' => $record->wallet->product->direct_price ?? null,
|
||||
'group_type' => $record->wallet->product->group_type ?? null,
|
||||
]);
|
||||
$this->applyProductTransactionUnit($record);
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveTransactionWallet($record): ?Wallet
|
||||
{
|
||||
if ($record instanceof Transaction) {
|
||||
$relation = $record->getRelationValue('wallet');
|
||||
if ($relation instanceof Wallet) {
|
||||
return $relation;
|
||||
}
|
||||
}
|
||||
|
||||
$walletId = (int) ($record->getAttributes()['wallet'] ?? 0);
|
||||
if ($walletId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Wallet::with('product')->find($walletId);
|
||||
}
|
||||
|
||||
private function resolveTransactionProduct($record): ?Product
|
||||
{
|
||||
$productId = $this->resolveAbshodeProductId($record) ?? 0;
|
||||
$wallet = $this->resolveTransactionWallet($record);
|
||||
|
||||
if ($productId <= 0) {
|
||||
$partial = $wallet?->product ?? $record->product ?? null;
|
||||
|
||||
return $partial instanceof Product ? $partial : null;
|
||||
}
|
||||
|
||||
$partial = $record->product ?? $wallet?->product ?? null;
|
||||
if ($partial instanceof Product && (int) $partial->id === $productId && trim((string) $partial->title) !== '') {
|
||||
return $partial;
|
||||
}
|
||||
|
||||
return Product::query()
|
||||
->select([
|
||||
'id',
|
||||
'title',
|
||||
'group_type',
|
||||
'unit',
|
||||
'direct_price',
|
||||
'amount',
|
||||
'trade_by_quantity',
|
||||
'metal_id',
|
||||
])
|
||||
->find($productId) ?? ($partial instanceof Product ? $partial : null);
|
||||
}
|
||||
|
||||
private function applyProductTransactionUnit($record): void
|
||||
{
|
||||
$product = $record->wallet->product ?? $record->product ?? null;
|
||||
$product = $this->resolveTransactionProduct($record);
|
||||
|
||||
if (!$product) {
|
||||
if (!empty($record->gold) && empty($record->wallet?->product)) {
|
||||
if (!empty($record->gold)) {
|
||||
$record->unit = self::tradeWeightUnit();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$record->unit = self::productQuantityUnit([
|
||||
'unit' => $product->unit ?? null,
|
||||
'direct_price' => $product->direct_price ?? null,
|
||||
'group_type' => $product->group_type ?? null,
|
||||
]);
|
||||
$payload = self::productUnitPayload($product);
|
||||
$onMainWallet = empty($record->wallet?->product_id);
|
||||
[$displayGold, $unit] = self::resolveTransactionDisplayGoldAndUnit(
|
||||
$payload,
|
||||
(float) ($record->gold ?? 0),
|
||||
$onMainWallet
|
||||
);
|
||||
|
||||
$record->gold = $displayGold;
|
||||
$record->unit = $unit;
|
||||
}
|
||||
|
||||
private function isProductWalletTransaction($record): bool
|
||||
{
|
||||
return !empty($record->wallet?->product);
|
||||
$wallet = $this->resolveTransactionWallet($record);
|
||||
|
||||
return !empty($wallet?->product);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,10 @@ class SingleOrderController extends Controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $request->all();
|
||||
$data['payment_product_id'] = $this->resolveGoldPaymentProductId($data);
|
||||
if (preg_match('/^gold:\d+$/', (string) ($data['method'] ?? ''))) {
|
||||
$data['method'] = 'gold';
|
||||
}
|
||||
$data['is_katod'] = ($data['method'] ?? null) === 'katod';
|
||||
if ($data['is_katod']) {
|
||||
$data['method'] = 'gold';
|
||||
@@ -245,6 +249,7 @@ class SingleOrderController extends Controller
|
||||
}
|
||||
$data['order'] = $order = Order::create($data);
|
||||
$abshode = $this->resolveAbshodeProductContext($data);
|
||||
$productId = (int) ($data['product_id'] ?? $abshode['id'] ?? 0);
|
||||
$buyTransaction = [
|
||||
"cash" => $data['payment_amount'],
|
||||
"gold" => $data['amount'],
|
||||
@@ -252,13 +257,11 @@ class SingleOrderController extends Controller
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'start_at' => now(),
|
||||
'title' => $abshode['title'] !== ''
|
||||
? self::productBuyTitle($abshode['title'])
|
||||
: self::meltedGoldBuyTitle(),
|
||||
'title' => $this->meltedOrProductBuyTitle($data),
|
||||
];
|
||||
if ($abshode['id'] > 0) {
|
||||
$buyTransaction['product_id'] = $abshode['id'];
|
||||
$buyTransaction['note'] = 'product_id:' . $abshode['id'];
|
||||
if ($productId > 0) {
|
||||
$buyTransaction['product_id'] = $productId;
|
||||
$buyTransaction['note'] = 'product_id:' . $productId;
|
||||
}
|
||||
new TransactionInterface($wallet_interface, $buyTransaction, 1, true, false);
|
||||
$response['1'] = 2;
|
||||
@@ -312,7 +315,7 @@ class SingleOrderController extends Controller
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'start_at' => now(),
|
||||
'title' => self::productBuyTitle($data['product']['title'] ?? ''),
|
||||
'title' => $this->meltedOrProductBuyTitle($data),
|
||||
], 1, true, false);
|
||||
|
||||
|
||||
@@ -332,22 +335,69 @@ class SingleOrderController extends Controller
|
||||
$data['total_fee'] = 0;
|
||||
$data['service_charge_price'] = $data['service_charge'] * $data['price_original'];
|
||||
$data['wallet'] = $wallet;
|
||||
if ($wallet->gold < $data['total_amount']) {
|
||||
|
||||
$paymentProductId = (int) ($data['payment_product_id'] ?? 0);
|
||||
$paymentWalletInterface = $wallet_interface;
|
||||
$paymentProduct = null;
|
||||
$paymentWallet = $wallet;
|
||||
|
||||
if ($paymentProductId > 0) {
|
||||
$paymentProduct = Product::find($paymentProductId);
|
||||
if (
|
||||
!$paymentProduct
|
||||
|| (int) ($paymentProduct->metal_id ?? 0) !== (int) ($data['product']['metal_id'] ?? 0)
|
||||
) {
|
||||
throw new UnprocessableEntityHttpException('روش پرداخت نامعتبر است.');
|
||||
}
|
||||
|
||||
$paymentWalletInterface = $this->productWalletInterface($paymentProductId);
|
||||
$paymentWallet = $paymentWalletInterface->wallet;
|
||||
if (!$paymentWallet) {
|
||||
throw new UnprocessableEntityHttpException('کیف پول یافت نشد.');
|
||||
}
|
||||
}
|
||||
|
||||
if ((float) $paymentWallet->gold < $data['total_amount']) {
|
||||
throw new UnprocessableEntityHttpException($this->insufficientGoldBalanceMessage($data));
|
||||
}
|
||||
if (!$data['product']['open_buy']) {
|
||||
throw new UnprocessableEntityHttpException('خرید این محصول در این لحظه مقدور نمیباشد');
|
||||
}
|
||||
$data['order'] = $order = Order::create($data);
|
||||
$data['transaction'][] = new TransactionInterface($wallet_interface, $_inp = [
|
||||
|
||||
$paymentSellTitle = $paymentProduct
|
||||
? self::productSellTitle($paymentProduct->title ?? '')
|
||||
: self::mainMetalSellTitle();
|
||||
|
||||
$paymentTransaction = [
|
||||
"number" => $data['number'],
|
||||
"gold" => $data['total_amount'],
|
||||
"product_id" => $data["product_id"],
|
||||
'expire_at' => now(),
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'title' => self::mainMetalSellTitle(),
|
||||
], 12, false, true);
|
||||
'title' => $paymentSellTitle,
|
||||
];
|
||||
|
||||
if ($paymentProductId > 0) {
|
||||
$paymentTransaction['product_id'] = $paymentProductId;
|
||||
$this->normalizeProductWalletGold($paymentWalletInterface);
|
||||
$data['transaction'][] = new TransactionInterface(
|
||||
$paymentWalletInterface,
|
||||
$paymentTransaction,
|
||||
1,
|
||||
false,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
$data['transaction'][] = new TransactionInterface(
|
||||
$paymentWalletInterface,
|
||||
$paymentTransaction,
|
||||
12,
|
||||
false,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
$wallet_interface_product = $this->productWalletInterface((int) $data["product_id"]);
|
||||
$this->normalizeProductWalletGold($wallet_interface_product);
|
||||
new TransactionInterface($wallet_interface_product, [
|
||||
@@ -357,7 +407,7 @@ class SingleOrderController extends Controller
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'start_at' => now(),
|
||||
'title' => self::productBuyTitle($data['product']['title'] ?? ''),
|
||||
'title' => $this->meltedOrProductBuyTitle($data),
|
||||
], 1, true, false);
|
||||
|
||||
}
|
||||
@@ -378,19 +428,18 @@ class SingleOrderController extends Controller
|
||||
}
|
||||
$data['order'] = $order = Order::create($data);
|
||||
$abshode = $this->resolveAbshodeProductContext($data);
|
||||
$productId = (int) ($data['product_id'] ?? $abshode['id'] ?? 0);
|
||||
$sellTransaction = [
|
||||
"gold" => $data['amount'],
|
||||
"cash" => $data['total_price'],
|
||||
'expire_at' => now(),
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'title' => $abshode['title'] !== ''
|
||||
? self::productSellTitle($abshode['title'])
|
||||
: self::meltedGoldSellTitle(),
|
||||
'title' => $this->meltedOrProductSellTitle($data),
|
||||
];
|
||||
if ($abshode['id'] > 0) {
|
||||
$sellTransaction['product_id'] = $abshode['id'];
|
||||
$sellTransaction['note'] = 'product_id:' . $abshode['id'];
|
||||
if ($productId > 0) {
|
||||
$sellTransaction['product_id'] = $productId;
|
||||
$sellTransaction['note'] = 'product_id:' . $productId;
|
||||
}
|
||||
new TransactionInterface($wallet_interface, $sellTransaction, 2, false, true);
|
||||
|
||||
@@ -428,7 +477,7 @@ class SingleOrderController extends Controller
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'start_at' => now(),
|
||||
'title' => self::productSellTitle($data['product']['title'] ?? ''),
|
||||
'title' => $this->meltedOrProductSellTitle($data),
|
||||
], 1, false, false);
|
||||
$data['transaction'][] = new TransactionInterface($wallet_interface, $_inp = [
|
||||
"number" => $data['number'],
|
||||
@@ -469,7 +518,7 @@ class SingleOrderController extends Controller
|
||||
'expire_at' => now(),
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'title' => self::mainMetalBuyTitle(),
|
||||
'title' => $this->meltedOrProductSellTitle($data),
|
||||
], 12, true, false);
|
||||
$this->normalizeProductWalletGold($wallet_interface_product);
|
||||
new TransactionInterface($wallet_interface_product, [
|
||||
@@ -479,7 +528,7 @@ class SingleOrderController extends Controller
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'start_at' => now(),
|
||||
'title' => self::productSellTitle($data['product']['title'] ?? ''),
|
||||
'title' => $this->meltedOrProductSellTitle($data),
|
||||
], 1, false, false);
|
||||
}
|
||||
|
||||
@@ -794,6 +843,20 @@ class SingleOrderController extends Controller
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolveGoldPaymentProductId(array $data): ?int
|
||||
{
|
||||
if (!empty($data['payment_product_id'])) {
|
||||
return (int) $data['payment_product_id'];
|
||||
}
|
||||
|
||||
$method = (string) ($data['method'] ?? '');
|
||||
if (preg_match('/^gold:(\d+)$/', $method, $matches)) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolveAbshodeProductContext(array $data): array
|
||||
{
|
||||
$productId = (int) ($data['product_id'] ?? $data['abshode_id'] ?? 0);
|
||||
@@ -825,17 +888,20 @@ class SingleOrderController extends Controller
|
||||
private function productTitleFromData(array $data): ?string
|
||||
{
|
||||
$product = $data['product'] ?? null;
|
||||
if (!$product) {
|
||||
return null;
|
||||
}
|
||||
$title = null;
|
||||
|
||||
if (is_array($product)) {
|
||||
$title = $product['title'] ?? null;
|
||||
} else {
|
||||
$title = $product->title ?? null;
|
||||
if ($product) {
|
||||
if (is_array($product)) {
|
||||
$title = $product['title'] ?? null;
|
||||
} else {
|
||||
$title = $product->title ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
$title = trim((string) $title);
|
||||
if ($title === '' && !empty($data['product_id'])) {
|
||||
$title = trim((string) Product::query()->whereKey((int) $data['product_id'])->value('title'));
|
||||
}
|
||||
|
||||
return $title !== '' ? $title : null;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class Product extends Model
|
||||
private static ?bool $useLastPriceIfZeroColumnExists = null;
|
||||
private static ?bool $directPriceColumnExists = null;
|
||||
private static ?bool $tradeByQuantityColumnExists = null;
|
||||
private static ?bool $inventoryColumnExists = null;
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
@@ -48,6 +49,9 @@ class Product extends Model
|
||||
if (!static::hasTradeByQuantityColumn()) {
|
||||
unset($product->attributes['trade_by_quantity']);
|
||||
}
|
||||
if (!static::hasInventoryColumn()) {
|
||||
unset($product->attributes['inventory']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -175,6 +179,24 @@ class Product extends Model
|
||||
return static::$tradeByQuantityColumnExists;
|
||||
}
|
||||
|
||||
public static function hasInventoryColumn(): bool
|
||||
{
|
||||
if (static::$inventoryColumnExists !== null) {
|
||||
return static::$inventoryColumnExists;
|
||||
}
|
||||
|
||||
try {
|
||||
static::$inventoryColumnExists = Schema::hasColumn(
|
||||
(new static)->getTable(),
|
||||
'inventory'
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
static::$inventoryColumnExists = false;
|
||||
}
|
||||
|
||||
return static::$inventoryColumnExists;
|
||||
}
|
||||
|
||||
public function orderLimitsEnabled(): bool
|
||||
{
|
||||
if (!static::hasOrderLimitColumns() || !($this->order_limit_enabled ?? false)) {
|
||||
|
||||
@@ -35,7 +35,7 @@ class AssetSummaryService
|
||||
->where('owner_type', $ownerType)
|
||||
->get();
|
||||
|
||||
$mainWallet = $userWallets->first(fn (Wallet $w) => $w->product_id === null);
|
||||
$mainWallet = $this->resolveMainWallet($userWallets);
|
||||
$metalIds = $this->catalogMetalIds($catalog);
|
||||
$weightUnit = Controller::tradeWeightUnit();
|
||||
$hasCountBased = $this->catalogHasCountBased($catalog);
|
||||
@@ -46,13 +46,13 @@ class AssetSummaryService
|
||||
// محصولات متصل به کیف اصلی (main_wallet_products) — موجودیشان = gold کیف اصلی
|
||||
$mainWalletProductIds = $this->configService->mainWalletProductIds();
|
||||
|
||||
$metalsWithProductWallet = $this->metalIdsWithProductWallet($userWallets, $catalogById, $mainWalletProductIds);
|
||||
$metalsWithProductWallet = $this->metalIdsWithProductWallet($userWallets, $catalogById, $mainWalletProductIds, $mainWallet);
|
||||
$inventoryCards = array_merge(
|
||||
$this->buildMeltedInventoryCards($catalog, $catalogById, $mainWallet, $metalIds, $metalsWithProductWallet, $mainWalletProductIds),
|
||||
$this->buildInventoryCards($userWallets, $catalogById, $mainWalletProductIds)
|
||||
$this->buildInventoryCards($userWallets, $catalogById, $mainWalletProductIds, $mainWallet),
|
||||
);
|
||||
$metalTotals = $this->buildMetalTotals($userWallets, $mainWallet, $metalIds, $weightUnit, $catalogById, $metalsWithProductWallet, $mainWalletProductIds);
|
||||
$cashWallet = (float) ($mainWallet->cash ?? 0);
|
||||
$cashWallet = (float) ($mainWallet?->cash ?? 0);
|
||||
$assetsRialValue = $this->sumAssetsRialValue(
|
||||
$metalTotals,
|
||||
$userWallets,
|
||||
@@ -112,7 +112,7 @@ class AssetSummaryService
|
||||
array $mainWalletProductIds = []
|
||||
): array {
|
||||
$cards = [];
|
||||
$mainWalletGrams = Controller::weightToGrams((float) ($mainWallet->gold ?? 0));
|
||||
$mainWalletGrams = Controller::weightToGrams((float) ($mainWallet?->gold ?? 0));
|
||||
$assignedMetalIds = [];
|
||||
$mainProductId = $this->configService->mainProductId();
|
||||
|
||||
@@ -123,7 +123,7 @@ class AssetSummaryService
|
||||
return [];
|
||||
}
|
||||
|
||||
$mainWalletWeight = (float) ($mainWallet->gold ?? 0);
|
||||
$mainWalletWeight = (float) ($mainWallet?->gold ?? 0);
|
||||
if ($mainWalletWeight <= 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -186,6 +186,66 @@ class AssetSummaryService
|
||||
return $cards;
|
||||
}
|
||||
|
||||
private function resolveMainWallet(Collection $userWallets): ?Wallet
|
||||
{
|
||||
return $userWallets->first(
|
||||
fn (Wallet $w) => $w->fund_id === null && $w->product_id === null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* کیف legacy محصول آبشده/کاتد — وقتی همان فلز روی کیف اصلی موجودی دارد نباید دوباره شمرده شود.
|
||||
*/
|
||||
private function shouldIgnoreLegacyProductWalletLine(
|
||||
Product $catalogProduct,
|
||||
?Wallet $mainWallet,
|
||||
Collection $catalogById,
|
||||
array $mainWalletProductIds
|
||||
): bool {
|
||||
if (in_array((int) $catalogProduct->id, $mainWalletProductIds, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((float) ($mainWallet?->gold ?? 0) <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isMeltedTradeProduct($catalogProduct)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mainMetalId = $this->resolveMainWalletMetalId($mainWallet, $catalogById, $mainWalletProductIds);
|
||||
|
||||
return $mainMetalId > 0 && (int) ($catalogProduct->metal_id ?? 0) === $mainMetalId;
|
||||
}
|
||||
|
||||
private function resolveMainWalletMetalId(
|
||||
?Wallet $mainWallet,
|
||||
Collection $catalogById,
|
||||
array $mainWalletProductIds
|
||||
): int {
|
||||
if ($mainWalletProductIds !== []) {
|
||||
$representative = $this->resolveMainWalletRepresentative($catalogById, $mainWalletProductIds);
|
||||
if ($representative) {
|
||||
return max(1, (int) ($representative->metal_id ?? 1));
|
||||
}
|
||||
}
|
||||
|
||||
if ((float) ($mainWallet?->gold ?? 0) <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$mainProductId = $this->configService->mainProductId();
|
||||
if ($mainProductId) {
|
||||
$mainProduct = $catalogById->get((int) $mainProductId);
|
||||
if ($mainProduct) {
|
||||
return max(1, (int) ($mainProduct->metal_id ?? 1));
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private function catalogProduct(Product $product, Collection $catalogById): Product
|
||||
{
|
||||
return $catalogById->get((int) $product->id) ?? $product;
|
||||
@@ -246,9 +306,14 @@ class AssetSummaryService
|
||||
}
|
||||
|
||||
/** فلزهایی که کاربر روی کیف محصول موجودی دارد — برای جلوگیری از کارت آبشدهٔ تکراری. */
|
||||
private function metalIdsWithProductWallet(Collection $userWallets, Collection $catalogById, array $mainWalletProductIds = []): array
|
||||
{
|
||||
private function metalIdsWithProductWallet(
|
||||
Collection $userWallets,
|
||||
Collection $catalogById,
|
||||
array $mainWalletProductIds = [],
|
||||
?Wallet $mainWallet = null
|
||||
): array {
|
||||
$metalIds = [];
|
||||
$mainWallet ??= $this->resolveMainWallet($userWallets);
|
||||
|
||||
foreach ($userWallets as $walletLine) {
|
||||
if ($walletLine->product_id === null || (float) $walletLine->gold <= 0) {
|
||||
@@ -265,7 +330,12 @@ class AssetSummaryService
|
||||
continue;
|
||||
}
|
||||
|
||||
$metalId = (int) ($this->catalogProduct($product, $catalogById)->metal_id ?? 0);
|
||||
$catalogProduct = $this->catalogProduct($product, $catalogById);
|
||||
if ($this->shouldIgnoreLegacyProductWalletLine($catalogProduct, $mainWallet, $catalogById, $mainWalletProductIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$metalId = (int) ($catalogProduct->metal_id ?? 0);
|
||||
if ($metalId > 0) {
|
||||
$metalIds[] = $metalId;
|
||||
}
|
||||
@@ -296,11 +366,20 @@ class AssetSummaryService
|
||||
return $catalog->first(fn (Product $product) => (int) ($product->metal_id ?? 0) === $metalId);
|
||||
}
|
||||
|
||||
private function buildInventoryCards(Collection $userWallets, Collection $catalogById, array $mainWalletProductIds = []): array
|
||||
{
|
||||
private function buildInventoryCards(
|
||||
Collection $userWallets,
|
||||
Collection $catalogById,
|
||||
array $mainWalletProductIds = [],
|
||||
?Wallet $mainWallet = null
|
||||
): array {
|
||||
$cards = [];
|
||||
$mainWallet ??= $this->resolveMainWallet($userWallets);
|
||||
|
||||
foreach ($userWallets as $walletLine) {
|
||||
if ($walletLine->fund_id !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($walletLine->product_id === null || (float) $walletLine->gold <= 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -316,6 +395,10 @@ class AssetSummaryService
|
||||
}
|
||||
|
||||
$catalogProduct = $this->catalogProduct($product, $catalogById);
|
||||
if ($this->shouldIgnoreLegacyProductWalletLine($catalogProduct, $mainWallet, $catalogById, $mainWalletProductIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$productPayload = $this->productPayload($catalogProduct);
|
||||
$isCountBased = $this->isCountBasedProduct($catalogProduct);
|
||||
|
||||
@@ -349,7 +432,7 @@ class AssetSummaryService
|
||||
return [];
|
||||
}
|
||||
|
||||
$mainWalletGrams = Controller::weightToGrams((float) ($mainWallet->gold ?? 0));
|
||||
$mainWalletGrams = Controller::weightToGrams((float) ($mainWallet?->gold ?? 0));
|
||||
$totals = [];
|
||||
|
||||
// فلز نمایندهٔ کیف اصلی در حالت main_wallet_products
|
||||
@@ -373,6 +456,10 @@ class AssetSummaryService
|
||||
}
|
||||
|
||||
$catalogProduct = $this->catalogProduct($walletLine->product, $catalogById);
|
||||
if ($this->shouldIgnoreLegacyProductWalletLine($catalogProduct, $mainWallet, $catalogById, $mainWalletProductIds)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((int) ($catalogProduct->metal_id ?? 0) !== (int) $metalId) {
|
||||
continue;
|
||||
}
|
||||
@@ -532,11 +619,16 @@ class AssetSummaryService
|
||||
'direct_price' => $product->direct_price ?? null,
|
||||
'group_type' => $product->group_type ?? null,
|
||||
'amount' => $product->amount ?? null,
|
||||
'trade_by_quantity' => $product->trade_by_quantity ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
private function productUnitWeightLabel(Product $product): ?string
|
||||
{
|
||||
if (!$this->isCountBasedProduct($product)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Controller::hasProductUnitWeight($this->productPayload($product))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Order;
|
||||
use App\Models\Product;
|
||||
|
||||
class BalanceReportService
|
||||
{
|
||||
public const METAL_LABELS = [
|
||||
1 => 'طلا',
|
||||
2 => 'نقره',
|
||||
3 => 'مس',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly ConfigService $configService
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
public function catalogMetalIds(): array
|
||||
{
|
||||
return Product::query()
|
||||
->where('active', 1)
|
||||
->whereNotNull('metal_id')
|
||||
->where('metal_id', '>', 0)
|
||||
->distinct()
|
||||
->orderBy('metal_id')
|
||||
->pluck('metal_id')
|
||||
->map(static fn ($id) => (int) $id)
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function metalLabel(int $metalId): string
|
||||
{
|
||||
return self::METAL_LABELS[$metalId] ?? "فلز {$metalId}";
|
||||
}
|
||||
|
||||
/** واحد نمایش موجودی/تراز هر فلز در ادمین — مس کیلوگرم، بقیه گرم */
|
||||
public function metalDisplayUnit(int $metalId): string
|
||||
{
|
||||
return (int) $metalId === 3 ? 'کیلوگرم' : 'گرم';
|
||||
}
|
||||
|
||||
public function gramsToMetalDisplay(float $grams, int $metalId): float
|
||||
{
|
||||
return (int) $metalId === 3 ? $grams / 1000.0 : $grams;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function catalogMetalsMeta(): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ($this->catalogMetalIds() as $metalId) {
|
||||
$metalId = (int) $metalId;
|
||||
$items[] = [
|
||||
'metal_id' => $metalId,
|
||||
'label' => $this->metalLabel($metalId),
|
||||
'display_unit' => $this->metalDisplayUnit($metalId),
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{gold: float, cash: float}>
|
||||
*/
|
||||
public function resolveOrderMetalSlices(Order $order): array
|
||||
{
|
||||
$order->loadMissing(['transactions.product']);
|
||||
|
||||
$slices = [];
|
||||
$txWithProduct = $order->transactions->filter(
|
||||
static fn ($tx) => $tx->product_id && abs((float) ($tx->gold ?? 0)) > 0
|
||||
);
|
||||
|
||||
if ($txWithProduct->isNotEmpty()) {
|
||||
foreach ($txWithProduct as $tx) {
|
||||
$metalId = (int) ($tx->product?->metal_id ?? 1);
|
||||
if ($metalId <= 0) {
|
||||
$metalId = 1;
|
||||
}
|
||||
$gold = abs((float) ($tx->gold ?? 0));
|
||||
if ($gold <= 0) {
|
||||
continue;
|
||||
}
|
||||
$slices[$metalId] = ($slices[$metalId] ?? 0.0) + $gold;
|
||||
}
|
||||
|
||||
$totalGold = array_sum($slices);
|
||||
$result = [];
|
||||
foreach ($slices as $metalId => $gold) {
|
||||
$cashShare = $totalGold > 0
|
||||
? (float) $order->total_price * ($gold / $totalGold)
|
||||
: 0.0;
|
||||
$result[(int) $metalId] = ['gold' => $gold, 'cash' => $cashShare];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$amount = (float) ($order->amount ?? 0);
|
||||
$totalPrice = (float) ($order->total_price ?? 0);
|
||||
if ($amount <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$metalId = 1;
|
||||
$tx = $order->transactions->first(static fn ($t) => $t->product_id);
|
||||
if ($tx?->product) {
|
||||
$metalId = max(1, (int) ($tx->product->metal_id ?? 1));
|
||||
}
|
||||
|
||||
return [$metalId => ['gold' => $amount, 'cash' => $totalPrice]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $metalIds
|
||||
* @return array<int, array{gold: float, cash: float, gold_abs: float, cash_abs: float}>
|
||||
*/
|
||||
public function initMetalBalanceBuckets(array $metalIds): array
|
||||
{
|
||||
$buckets = [];
|
||||
foreach ($metalIds as $metalId) {
|
||||
$buckets[(int) $metalId] = [
|
||||
'gold' => 0.0,
|
||||
'cash' => 0.0,
|
||||
'gold_abs' => 0.0,
|
||||
'cash_abs' => 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
return $buckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{gold: float, cash: float, gold_abs: float, cash_abs: float}> $buckets
|
||||
*/
|
||||
public function applyOrderToBuckets(array &$buckets, Order $order): void
|
||||
{
|
||||
$slices = $this->resolveOrderMetalSlices($order);
|
||||
foreach ($slices as $metalId => $slice) {
|
||||
$metalId = (int) $metalId;
|
||||
if (! isset($buckets[$metalId])) {
|
||||
$buckets[$metalId] = [
|
||||
'gold' => 0.0,
|
||||
'cash' => 0.0,
|
||||
'gold_abs' => 0.0,
|
||||
'cash_abs' => 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
$gold = (float) $slice['gold'];
|
||||
$cash = (float) $slice['cash'];
|
||||
|
||||
if ((int) $order->direction === 1) {
|
||||
$buckets[$metalId]['gold'] += $gold;
|
||||
$buckets[$metalId]['cash'] -= $cash;
|
||||
} elseif ((int) $order->direction === 2) {
|
||||
$buckets[$metalId]['gold'] -= $gold;
|
||||
$buckets[$metalId]['cash'] += $cash;
|
||||
}
|
||||
|
||||
$buckets[$metalId]['gold_abs'] += $gold;
|
||||
$buckets[$metalId]['cash_abs'] += $cash;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{gold: float, cash: float, gold_abs: float, cash_abs: float}> $limanBuckets
|
||||
* @param array<int, array{gold: float, cash: float, gold_abs: float, cash_abs: float}> $kalhorBuckets
|
||||
*/
|
||||
public function buildMetalBalancesResponse(array $limanBuckets, array $kalhorBuckets): array
|
||||
{
|
||||
$metalIds = array_values(array_unique(array_merge(
|
||||
array_keys($limanBuckets),
|
||||
array_keys($kalhorBuckets)
|
||||
)));
|
||||
sort($metalIds);
|
||||
|
||||
$items = [];
|
||||
$totalGoldBalance = 0.0;
|
||||
|
||||
foreach ($metalIds as $metalId) {
|
||||
$metalId = (int) $metalId;
|
||||
$liman = $limanBuckets[$metalId] ?? [
|
||||
'gold' => 0.0,
|
||||
'cash' => 0.0,
|
||||
'gold_abs' => 0.0,
|
||||
'cash_abs' => 0.0,
|
||||
];
|
||||
$kalhor = $kalhorBuckets[$metalId] ?? [
|
||||
'gold' => 0.0,
|
||||
'cash' => 0.0,
|
||||
'gold_abs' => 0.0,
|
||||
'cash_abs' => 0.0,
|
||||
];
|
||||
|
||||
$goldBalance = -$liman['gold'] + $kalhor['gold'];
|
||||
$cashBalance = -$liman['cash'] + $kalhor['cash'];
|
||||
$goldBalanceOfLiman = -$liman['gold'];
|
||||
$goldBalanceOfKalhor = $kalhor['gold'];
|
||||
|
||||
$items[] = [
|
||||
'metal_id' => $metalId,
|
||||
'label' => $this->metalLabel($metalId),
|
||||
'balance' => round($goldBalance, 4),
|
||||
'goldBalance' => round($goldBalance, 4),
|
||||
'cashBalance' => round($cashBalance, 2),
|
||||
'goldBalanceOfLiman' => round($goldBalanceOfLiman, 4),
|
||||
'goldBalanceOfKalhor' => round($goldBalanceOfKalhor, 4),
|
||||
];
|
||||
|
||||
$totalGoldBalance += $goldBalance;
|
||||
}
|
||||
|
||||
return [
|
||||
'items' => $items,
|
||||
'totalGoldBalance' => round($totalGoldBalance, 4),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<int, array{gold: float, cash: float, gold_abs: float, cash_abs: float}> $buckets */
|
||||
public function applyKalhorSlice(
|
||||
array &$buckets,
|
||||
float $gold,
|
||||
float $cash,
|
||||
int $direction,
|
||||
int $metalId = 1
|
||||
): void {
|
||||
if (! isset($buckets[$metalId])) {
|
||||
$buckets[$metalId] = [
|
||||
'gold' => 0.0,
|
||||
'cash' => 0.0,
|
||||
'gold_abs' => 0.0,
|
||||
'cash_abs' => 0.0,
|
||||
];
|
||||
}
|
||||
|
||||
if ($direction === 2) {
|
||||
$buckets[$metalId]['gold'] += $gold;
|
||||
$buckets[$metalId]['cash'] -= $cash;
|
||||
} elseif ($direction === 1) {
|
||||
$buckets[$metalId]['gold'] -= $gold;
|
||||
$buckets[$metalId]['cash'] += $cash;
|
||||
}
|
||||
|
||||
$buckets[$metalId]['gold_abs'] += $gold;
|
||||
$buckets[$metalId]['cash_abs'] += $cash;
|
||||
}
|
||||
|
||||
public function resolveTransactionMetalId(mixed $transaction): int
|
||||
{
|
||||
$product = $this->transactionProduct($transaction);
|
||||
if ($product !== null) {
|
||||
$metalId = (int) ($product->metal_id ?? 0);
|
||||
|
||||
return $metalId > 0 ? $metalId : 1;
|
||||
}
|
||||
|
||||
$walletProduct = $this->transactionWalletProduct($transaction);
|
||||
if ($walletProduct !== null) {
|
||||
$metalId = (int) ($walletProduct->metal_id ?? 0);
|
||||
|
||||
return $metalId > 0 ? $metalId : 1;
|
||||
}
|
||||
|
||||
if ($this->isMainWalletMetalTransaction($transaction)) {
|
||||
return $this->configService->mainWalletMetalId();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function resolveTransactionDisplayTypeStr(mixed $transaction, ?int $metalId = null): ?string
|
||||
{
|
||||
$type = (int) ($this->lineValue($transaction, 'type') ?? 0);
|
||||
if (!in_array($type, [1, 2, 7, 10], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$metalId ??= $this->resolveTransactionMetalId($transaction);
|
||||
$label = $this->metalLabel($metalId);
|
||||
|
||||
return match ($type) {
|
||||
1 => 'خرید ' . $label,
|
||||
2 => 'فروش ' . $label,
|
||||
7 => 'واریز ' . $label,
|
||||
10 => 'برداشت ' . $label,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
public function transactionGoldGrams(mixed $transaction): float
|
||||
{
|
||||
$gold = (float) ($this->lineValue($transaction, 'gold') ?? 0);
|
||||
if (abs($gold) < 0.0000001) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$product = $this->transactionProduct($transaction);
|
||||
if ($product !== null) {
|
||||
return Controller::productLineWeightToGrams($product, $gold);
|
||||
}
|
||||
|
||||
return Controller::weightToGrams($gold);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $catalogMetalIds
|
||||
* @return array<string, float>
|
||||
*/
|
||||
public function buildTransactionMetalDisplayMap(mixed $transaction, array $catalogMetalIds): array
|
||||
{
|
||||
$map = [];
|
||||
foreach ($catalogMetalIds as $metalId) {
|
||||
$map[(string) (int) $metalId] = 0.0;
|
||||
}
|
||||
|
||||
$type = (int) ($this->lineValue($transaction, 'type') ?? 0);
|
||||
if (in_array($type, [3, 4], true)) {
|
||||
return $map;
|
||||
}
|
||||
|
||||
$gold = (float) ($this->lineValue($transaction, 'gold') ?? 0);
|
||||
if (abs($gold) < 0.0000001) {
|
||||
return $map;
|
||||
}
|
||||
|
||||
$metalId = $this->resolveTransactionMetalId($transaction);
|
||||
$grams = abs($this->transactionGoldGrams($transaction));
|
||||
$display = $this->gramsToMetalDisplay($grams, $metalId);
|
||||
if ($gold < 0) {
|
||||
$display *= -1;
|
||||
}
|
||||
|
||||
$map[(string) $metalId] = round($display, 3);
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $catalogMetalIds
|
||||
*/
|
||||
public function enrichTransactionTableRow(mixed &$row, array $catalogMetalIds): void
|
||||
{
|
||||
$metalMap = $this->buildTransactionMetalDisplayMap($row, $catalogMetalIds);
|
||||
$metalId = $this->resolveTransactionMetalId($row);
|
||||
$metalLabel = $this->metalLabel($metalId);
|
||||
$displayType = $this->resolveTransactionDisplayTypeStr($row, $metalId);
|
||||
|
||||
if (is_array($row)) {
|
||||
$row['metal_gold'] = $metalMap;
|
||||
$row['transaction_metal_id'] = $metalId;
|
||||
$row['transaction_metal_label'] = $metalLabel;
|
||||
if ($displayType !== null) {
|
||||
$row['type_str'] = $displayType;
|
||||
}
|
||||
foreach ($catalogMetalIds as $mid) {
|
||||
$row['metal_gold_' . $mid] = $metalMap[(string) $mid] ?? 0.0;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_object($row)) {
|
||||
$row->metal_gold = $metalMap;
|
||||
$row->transaction_metal_id = $metalId;
|
||||
$row->transaction_metal_label = $metalLabel;
|
||||
if ($displayType !== null) {
|
||||
$row->type_str = $displayType;
|
||||
}
|
||||
foreach ($catalogMetalIds as $mid) {
|
||||
$row->{'metal_gold_' . $mid} = $metalMap[(string) $mid] ?? 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function transactionProduct(mixed $transaction): ?object
|
||||
{
|
||||
if (is_object($transaction) && isset($transaction->product)) {
|
||||
return $transaction->product;
|
||||
}
|
||||
|
||||
if (is_array($transaction) && isset($transaction['product'])) {
|
||||
$product = $transaction['product'];
|
||||
|
||||
return is_object($product) ? $product : (object) $product;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function transactionWalletProduct(mixed $transaction): ?object
|
||||
{
|
||||
$wallet = $this->lineValue($transaction, 'wallet');
|
||||
if ($wallet === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_object($wallet)) {
|
||||
return isset($wallet->product) ? $wallet->product : null;
|
||||
}
|
||||
|
||||
if (is_array($wallet) && isset($wallet['product'])) {
|
||||
$product = $wallet['product'];
|
||||
|
||||
return is_object($product) ? $product : (object) $product;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isMainWalletMetalTransaction(mixed $transaction): bool
|
||||
{
|
||||
$type = (int) ($this->lineValue($transaction, 'type') ?? 0);
|
||||
if (!in_array($type, [1, 2, 7, 10], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$wallet = $this->lineValue($transaction, 'wallet');
|
||||
if ($wallet === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_object($wallet)) {
|
||||
return $wallet->product_id === null && $wallet->fund_id === null;
|
||||
}
|
||||
|
||||
if (is_array($wallet)) {
|
||||
return ($wallet['product_id'] ?? null) === null && ($wallet['fund_id'] ?? null) === null;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function resolveOrderMetalId(mixed $order): int
|
||||
{
|
||||
$product = $this->resolveOrderProduct($order);
|
||||
if ($product !== null) {
|
||||
$metalId = (int) ($product->metal_id ?? 0);
|
||||
|
||||
return $metalId > 0 ? $metalId : 1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $catalogMetalIds
|
||||
*/
|
||||
public function enrichOrderTableRow(mixed &$row, array $catalogMetalIds = []): void
|
||||
{
|
||||
$product = $this->resolveOrderProduct($row);
|
||||
$unit = $this->resolveOrderAmountUnit($product);
|
||||
$amount = (float) ($this->lineValue($row, 'amount') ?? 0);
|
||||
$displayAmount = $this->resolveOrderDisplayAmount($amount, $product);
|
||||
$metalId = $this->resolveOrderMetalId($row);
|
||||
$metalLabel = $this->metalLabel($metalId);
|
||||
$price = (float) ($this->lineValue($row, 'price') ?? 0);
|
||||
|
||||
$metalPrices = [];
|
||||
foreach ($catalogMetalIds as $mid) {
|
||||
$mid = (int) $mid;
|
||||
$metalPrices[(string) $mid] = $mid === $metalId ? $price : 0.0;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'amount_unit' => $unit,
|
||||
'amount_display' => $displayAmount,
|
||||
'order_metal_id' => $metalId,
|
||||
'order_metal_label' => $metalLabel,
|
||||
'metal_prices' => $metalPrices,
|
||||
];
|
||||
|
||||
foreach ($catalogMetalIds as $mid) {
|
||||
$payload['metal_price_' . (int) $mid] = $metalPrices[(string) (int) $mid] ?? 0.0;
|
||||
}
|
||||
|
||||
if (is_array($row)) {
|
||||
foreach ($payload as $key => $value) {
|
||||
$row[$key] = $value;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_object($row)) {
|
||||
foreach ($payload as $key => $value) {
|
||||
$row->{$key} = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function resolveOrderAmountUnit(?object $product): string
|
||||
{
|
||||
if ($product === null) {
|
||||
return Controller::tradeWeightUnit();
|
||||
}
|
||||
|
||||
return Controller::productQuantityUnit(Controller::productUnitPayload($product));
|
||||
}
|
||||
|
||||
private function resolveOrderDisplayAmount(float $amount, ?object $product): float
|
||||
{
|
||||
if ($product === null || abs($amount) < 0.0000001) {
|
||||
return round($amount, 3);
|
||||
}
|
||||
|
||||
$payload = Controller::productUnitPayload($product);
|
||||
$productUnit = trim((string) ($payload['unit'] ?? ''));
|
||||
if (!Controller::isWeightBasedProductUnit($productUnit)) {
|
||||
return round($amount, 3);
|
||||
}
|
||||
|
||||
$tradeUnit = Controller::tradeWeightUnit();
|
||||
if ($productUnit !== $tradeUnit) {
|
||||
return round(Controller::convertWeight($amount, $tradeUnit, $productUnit), 3);
|
||||
}
|
||||
|
||||
return round($amount, 3);
|
||||
}
|
||||
|
||||
private function resolveOrderProduct(mixed $order): ?object
|
||||
{
|
||||
$transactions = $this->lineValue($order, 'transactions');
|
||||
if ($transactions instanceof \Illuminate\Support\Collection) {
|
||||
$transactions = $transactions->all();
|
||||
}
|
||||
|
||||
if (!is_array($transactions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($transactions as $tx) {
|
||||
$product = is_object($tx) ? ($tx->product ?? null) : ($tx['product'] ?? null);
|
||||
if ($product) {
|
||||
return is_object($product) ? $product : (object) $product;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function lineValue(mixed $line, string $key): mixed
|
||||
{
|
||||
if (is_array($line)) {
|
||||
return $line[$key] ?? null;
|
||||
}
|
||||
|
||||
if (is_object($line)) {
|
||||
return $line->{$key} ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -97,27 +97,22 @@ class CategoryService
|
||||
{
|
||||
$names = $this->collectTargetCategoryNames();
|
||||
$created = 0;
|
||||
$order = 0;
|
||||
$maxOrder = (int) Category::max('sort_order');
|
||||
|
||||
foreach ($names as $name) {
|
||||
$order++;
|
||||
$existing = Category::where('name', $name)->first();
|
||||
|
||||
if ($existing) {
|
||||
if ((int) $existing->sort_order !== $order) {
|
||||
$existing->sort_order = $order;
|
||||
$existing->save();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$maxOrder++;
|
||||
Category::create([
|
||||
'name' => $name,
|
||||
'active' => 1,
|
||||
'show_in_filter' => 1,
|
||||
'show_in_select' => 1,
|
||||
'sort_order' => $order,
|
||||
'sort_order' => $maxOrder,
|
||||
]);
|
||||
$created++;
|
||||
}
|
||||
|
||||
@@ -78,12 +78,9 @@ class MontzarService
|
||||
|
||||
$response = Http::timeout(7)
|
||||
->acceptJson()
|
||||
->withOptions([
|
||||
'curl' => [
|
||||
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
|
||||
CURLOPT_TCP_FASTOPEN => 1,
|
||||
],
|
||||
])
|
||||
->withOptions(self::httpClientOptions(
|
||||
defined('CURLOPT_TCP_FASTOPEN') ? [(int) constant('CURLOPT_TCP_FASTOPEN') => 1] : []
|
||||
))
|
||||
->withToken($token)
|
||||
->get($this->endpoint('/server/customer/product/list'))
|
||||
->json();
|
||||
|
||||
@@ -30,7 +30,22 @@ class ProductDisplayPriceService
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->isMeltedTradeProduct($product);
|
||||
return $this->isMeltedTradeProduct($product)
|
||||
|| $this->usesQuantityAppPrice($product);
|
||||
}
|
||||
|
||||
/** معامله مقداری با قیمت اپ — نه قیمت دستی قدیمی با price پر در DB */
|
||||
private function usesQuantityAppPrice(Product $product): bool
|
||||
{
|
||||
if (! ($product->trade_by_quantity ?? false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($product->reading_method || (int) ($product->direct_price ?? 0) === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (int) ($product->price ?? 0) <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +88,19 @@ class ProductDisplayPriceService
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($product->reading_method && ! $this->productUsesLastPriceIfZero($product)) {
|
||||
$product->setAttribute('reference_provider_active', false);
|
||||
$this->zeroProductPrice($product);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($product->reading_method) {
|
||||
$product->setAttribute('reference_provider_active', true);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->isWeightBasedStoreProduct($product)) {
|
||||
$this->enrichWeightBasedPrice($product);
|
||||
}
|
||||
@@ -262,25 +290,12 @@ class ProductDisplayPriceService
|
||||
/** @param array<int, array<string, mixed>> $feed */
|
||||
private function applyReferenceFeedPrice(Product $product, array $feed, ZaarboxPriceService $zaarbox): void
|
||||
{
|
||||
if (Product::hasStoredPriceColumns()) {
|
||||
$stored = (int) ($product->price ?? 0) + (int) ($product->price_profit ?? 0);
|
||||
if ($stored > 0) {
|
||||
$product->setAttribute('price', (int) ($product->price ?? 0));
|
||||
if (Schema::hasColumn($product->getTable(), 'price_profit')) {
|
||||
$product->setAttribute('price_profit', (int) ($product->price_profit ?? 0));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$useLast = $this->productUsesLastPriceIfZero($product);
|
||||
$row = $zaarbox->findFeedRow($feed, (string) $product->reading_method);
|
||||
if ($row === null) {
|
||||
$useLast = $this->productUsesLastPriceIfZero($product);
|
||||
$product->setAttribute('reference_provider_active', $useLast);
|
||||
$offset = ProductPriceService::offsetPercent($product);
|
||||
$historical = app(ConfigService::class)->getBool('show_last_price', false)
|
||||
? $this->resolveHistoricalBasePrice()
|
||||
: 0;
|
||||
$historical = $useLast ? $this->resolveHistoricalBasePrice() : 0;
|
||||
$computed = $historical > 0
|
||||
? ProductPriceService::adjustedTotal($historical, 0, $offset)
|
||||
: 0;
|
||||
@@ -289,9 +304,19 @@ class ProductDisplayPriceService
|
||||
return;
|
||||
}
|
||||
|
||||
$providerRaw = $zaarbox->resolveReferenceProviderTotal($row);
|
||||
$providerActive = $useLast || $providerRaw > 0;
|
||||
$product->setAttribute('reference_provider_active', $providerActive);
|
||||
|
||||
if (! $useLast && $providerRaw <= 0) {
|
||||
$this->zeroProductPrice($product);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$computed = $zaarbox->computeAdjustedProductPrice($product, $row);
|
||||
if ($computed <= 0) {
|
||||
if ($this->productUsesLastPriceIfZero($product)) {
|
||||
if ($useLast) {
|
||||
$fallback = $this->resolveLastPriceFallback($product);
|
||||
if ($fallback > 0) {
|
||||
$product->setAttribute('price', $fallback);
|
||||
@@ -424,7 +449,7 @@ class ProductDisplayPriceService
|
||||
|
||||
$row = app(ZaarboxPriceService::class)->normalizeFeedRow($row);
|
||||
|
||||
return (int) round((float) ($row['price'] ?? 0) + (float) ($row['profit'] ?? 0));
|
||||
return app(ZaarboxPriceService::class)->resolveReferenceProviderTotal($row);
|
||||
}
|
||||
|
||||
private function isMeltedTradeProduct(Product $product): bool
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Product;
|
||||
use Rooyesh\Wallet\models\Wallet;
|
||||
|
||||
class TransactionMetalService
|
||||
{
|
||||
private const METAL_LABELS = [
|
||||
1 => 'طلا',
|
||||
2 => 'نقره',
|
||||
3 => 'مس',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly ConfigService $configService
|
||||
) {
|
||||
}
|
||||
|
||||
public function metalLabel(int $metalId): string
|
||||
{
|
||||
return self::METAL_LABELS[$metalId] ?? Controller::metalTypeLabel();
|
||||
}
|
||||
|
||||
/** @return array<int, array{metal_id: int, label: string, display_unit: string}> */
|
||||
public function catalogMetals(): array
|
||||
{
|
||||
return Product::query()
|
||||
->where('active', 1)
|
||||
->pluck('metal_id')
|
||||
->filter(fn ($id) => (int) $id > 0)
|
||||
->map(fn ($id) => (int) $id)
|
||||
->unique()
|
||||
->sort()
|
||||
->values()
|
||||
->map(fn (int $metalId) => [
|
||||
'metal_id' => $metalId,
|
||||
'label' => $this->metalLabel($metalId),
|
||||
'display_unit' => Controller::tradeWeightUnit(),
|
||||
])
|
||||
->all();
|
||||
}
|
||||
|
||||
public function resolveMetalIdForTransaction(object $transaction): int
|
||||
{
|
||||
$productId = (int) ($transaction->product_id ?? 0);
|
||||
if ($productId > 0) {
|
||||
$metalId = (int) (Product::query()->whereKey($productId)->value('metal_id') ?? 0);
|
||||
if ($metalId > 0) {
|
||||
return $metalId;
|
||||
}
|
||||
}
|
||||
|
||||
$wallet = $transaction->wallet ?? null;
|
||||
if (is_numeric($wallet)) {
|
||||
$wallet = Wallet::with('product:id,metal_id')->find($wallet);
|
||||
} elseif (is_object($wallet) && !$wallet->relationLoaded('product')) {
|
||||
$wallet->load('product:id,metal_id');
|
||||
}
|
||||
|
||||
if (is_object($wallet) && $wallet->product_id && $wallet->product) {
|
||||
$metalId = (int) ($wallet->product->metal_id ?? 0);
|
||||
if ($metalId > 0) {
|
||||
return $metalId;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isMainWalletMetalTransaction($transaction, $wallet)) {
|
||||
return $this->configService->mainWalletMetalId();
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
private function isMainWalletMetalTransaction(object $transaction, mixed $wallet): bool
|
||||
{
|
||||
$type = (int) ($transaction->type ?? 0);
|
||||
if (!in_array($type, [1, 2, 7, 10], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$wallet || !is_object($wallet)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $wallet->product_id === null && $wallet->fund_id === null;
|
||||
}
|
||||
|
||||
public function resolveDisplayTypeStr(object $transaction): ?string
|
||||
{
|
||||
$type = (int) ($transaction->type ?? 0);
|
||||
if (!in_array($type, [1, 2, 7, 10], true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$metalLabel = $this->metalLabel($this->resolveMetalIdForTransaction($transaction));
|
||||
|
||||
return match ($type) {
|
||||
1 => Controller::productBuyTitle($metalLabel),
|
||||
2 => Controller::productSellTitle($metalLabel),
|
||||
7 => 'واریز ' . $metalLabel,
|
||||
10 => 'برداشت ' . $metalLabel,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/** @param iterable<int, array<string, mixed>|object> $rows */
|
||||
public function enrichTransactionRows(iterable $rows): array
|
||||
{
|
||||
$metalIds = array_column($this->catalogMetals(), 'metal_id');
|
||||
$enriched = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$record = is_array($row) ? (object) $row : $row;
|
||||
$metalId = $this->resolveMetalIdForTransaction($record);
|
||||
$displayType = $this->resolveDisplayTypeStr($record);
|
||||
$type = (int) (is_array($row) ? ($row['type'] ?? 0) : ($row->type ?? 0));
|
||||
$gold = (float) (is_array($row) ? ($row['gold'] ?? 0) : ($row->gold ?? 0));
|
||||
$isMoney = in_array($type, [3, 4], true);
|
||||
|
||||
if ($displayType !== null) {
|
||||
if (is_array($row)) {
|
||||
$row['type_str'] = $displayType;
|
||||
} else {
|
||||
$row->type_str = $displayType;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($metalIds as $mid) {
|
||||
$key = "metal_gold_{$mid}";
|
||||
$value = (!$isMoney && $metalId === $mid) ? $gold : 0;
|
||||
if (is_array($row)) {
|
||||
$row[$key] = $value;
|
||||
} else {
|
||||
$row->{$key} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($row)) {
|
||||
$row['transaction_metal_id'] = $metalId;
|
||||
$row['transaction_metal_label'] = $this->metalLabel($metalId);
|
||||
} else {
|
||||
$row->transaction_metal_id = $metalId;
|
||||
$row->transaction_metal_label = $this->metalLabel($metalId);
|
||||
}
|
||||
|
||||
$enriched[] = $row;
|
||||
}
|
||||
|
||||
return $enriched;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Price;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
@@ -12,8 +15,10 @@ use Illuminate\Support\Collection;
|
||||
*/
|
||||
class WalletBalanceService
|
||||
{
|
||||
public function __construct(private ConfigService $configService)
|
||||
{
|
||||
public function __construct(
|
||||
private ConfigService $configService,
|
||||
private BalanceReportService $balanceReportService
|
||||
) {
|
||||
}
|
||||
|
||||
public function mainWalletProductIds(): array
|
||||
@@ -79,6 +84,240 @@ class WalletBalanceService
|
||||
return $sum;
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
public function catalogMetalIds(): array
|
||||
{
|
||||
$ids = $this->balanceReportService->catalogMetalIds();
|
||||
|
||||
return $ids !== [] ? $ids : [1];
|
||||
}
|
||||
|
||||
public function resolveMainWalletMetalId(): int
|
||||
{
|
||||
$mainWalletProductIds = $this->mainWalletProductIds();
|
||||
if ($mainWalletProductIds !== []) {
|
||||
$product = Product::query()
|
||||
->whereIn('id', $mainWalletProductIds)
|
||||
->orderBy('id')
|
||||
->first();
|
||||
|
||||
return max(1, (int) ($product?->metal_id ?? 1));
|
||||
}
|
||||
|
||||
$metalIds = $this->catalogMetalIds();
|
||||
|
||||
return count($metalIds) === 1 ? (int) $metalIds[0] : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* فلز مرتبط با یک ردیف کیف در جدول ادمین.
|
||||
*
|
||||
* @param Collection|array<int, mixed>|null $wallets
|
||||
*/
|
||||
public function resolveWalletLineMetalId(mixed $line, ?int $mainWalletMetalId = null): int
|
||||
{
|
||||
$fundId = $this->lineValue($line, 'fund_id');
|
||||
$productId = $this->lineValue($line, 'product_id');
|
||||
|
||||
if ($fundId !== null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($productId !== null) {
|
||||
$product = $this->lineProduct($line);
|
||||
$metalId = (int) ($product?->metal_id ?? 0);
|
||||
|
||||
return $metalId > 0 ? $metalId : 1;
|
||||
}
|
||||
|
||||
return $mainWalletMetalId ?? $this->resolveMainWalletMetalId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $catalogMetalIds
|
||||
* @return array<string, float>
|
||||
*/
|
||||
public function walletLineMetalGoldMap(
|
||||
mixed $line,
|
||||
array $catalogMetalIds,
|
||||
?int $mainWalletMetalId = null
|
||||
): array {
|
||||
$map = [];
|
||||
foreach ($catalogMetalIds as $metalId) {
|
||||
$map[(string) $metalId] = 0.0;
|
||||
}
|
||||
|
||||
$goldGrams = $this->walletLineGoldGrams($line);
|
||||
if ($goldGrams <= 0) {
|
||||
return $map;
|
||||
}
|
||||
|
||||
$metalId = $this->resolveWalletLineMetalId($line, $mainWalletMetalId);
|
||||
$map[(string) $metalId] = round(
|
||||
$this->balanceReportService->gramsToMetalDisplay($goldGrams, $metalId),
|
||||
3
|
||||
);
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection|array<int, mixed> $walletLines
|
||||
* @param list<int> $catalogMetalIds
|
||||
* @return array<string, float> جمع به گرم
|
||||
*/
|
||||
public function sumWalletMetalGrams(Collection|array $walletLines, array $catalogMetalIds): array
|
||||
{
|
||||
$buckets = [];
|
||||
foreach ($catalogMetalIds as $metalId) {
|
||||
$buckets[(string) (int) $metalId] = 0.0;
|
||||
}
|
||||
|
||||
$mainWalletMetalId = $this->resolveMainWalletMetalId();
|
||||
foreach ($this->walletLines($walletLines) as $line) {
|
||||
$goldGrams = $this->walletLineGoldGrams($line);
|
||||
if ($goldGrams <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$metalId = (string) $this->resolveWalletLineMetalId($line, $mainWalletMetalId);
|
||||
$buckets[$metalId] = ($buckets[$metalId] ?? 0.0) + $goldGrams;
|
||||
}
|
||||
|
||||
return $buckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection|array<int, mixed> $walletLines
|
||||
* @param list<int> $catalogMetalIds
|
||||
* @return list<array{metal_id: int, label: string, gold: float, display_unit: string, goldToCash: float}>
|
||||
*/
|
||||
public function buildWalletMetalSums(Collection|array $walletLines, array $catalogMetalIds): array
|
||||
{
|
||||
$gramBuckets = $this->sumWalletMetalGrams($walletLines, $catalogMetalIds);
|
||||
|
||||
$items = [];
|
||||
foreach ($catalogMetalIds as $metalId) {
|
||||
$metalId = (int) $metalId;
|
||||
$goldGrams = (float) ($gramBuckets[(string) $metalId] ?? 0.0);
|
||||
$displayGold = round($this->balanceReportService->gramsToMetalDisplay($goldGrams, $metalId), 3);
|
||||
$price = $this->livePricePerGram($metalId);
|
||||
$items[] = [
|
||||
'metal_id' => $metalId,
|
||||
'label' => $this->balanceReportService->metalLabel($metalId),
|
||||
'gold' => $displayGold,
|
||||
'display_unit' => $this->balanceReportService->metalDisplayUnit($metalId),
|
||||
'goldToCash' => round($goldGrams * $price, 2),
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection|array<int, mixed> $walletLines
|
||||
* @param list<int> $catalogMetalIds
|
||||
* @return array<string, float> مقادیر نمایشی
|
||||
*/
|
||||
public function buildCustomerWalletMetalDisplayMap(Collection|array $walletLines, array $catalogMetalIds): array
|
||||
{
|
||||
$gramBuckets = $this->sumWalletMetalGrams($walletLines, $catalogMetalIds);
|
||||
$map = [];
|
||||
|
||||
foreach ($catalogMetalIds as $metalId) {
|
||||
$metalId = (int) $metalId;
|
||||
$goldGrams = (float) ($gramBuckets[(string) $metalId] ?? 0.0);
|
||||
$map[(string) $metalId] = round(
|
||||
$this->balanceReportService->gramsToMetalDisplay($goldGrams, $metalId),
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $catalogMetalIds
|
||||
*/
|
||||
public function enrichWalletTableRow(mixed &$row, array $catalogMetalIds, int $mainWalletMetalId): void
|
||||
{
|
||||
$metalMap = $this->walletLineMetalGoldMap($row, $catalogMetalIds, $mainWalletMetalId);
|
||||
|
||||
if (is_array($row)) {
|
||||
$row['metal_gold'] = $metalMap;
|
||||
foreach ($catalogMetalIds as $metalId) {
|
||||
$row['metal_gold_' . $metalId] = $metalMap[(string) $metalId] ?? 0.0;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_object($row)) {
|
||||
$row->metal_gold = $metalMap;
|
||||
foreach ($catalogMetalIds as $metalId) {
|
||||
$row->{'metal_gold_' . $metalId} = $metalMap[(string) $metalId] ?? 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function livePricePerGram(int $metalId): float
|
||||
{
|
||||
if ($metalId === 1) {
|
||||
$live = (int) app(ProductDisplayPriceService::class)->resolveAppBasePrice();
|
||||
if ($live > 0) {
|
||||
return (float) $live;
|
||||
}
|
||||
|
||||
return (float) (Price::latest('id')->value('price') ?? 0);
|
||||
}
|
||||
|
||||
$product = Product::query()
|
||||
->where('active', 1)
|
||||
->where('metal_id', $metalId)
|
||||
->where('price', '>', 0)
|
||||
->orderBy('id')
|
||||
->first();
|
||||
|
||||
if (!$product) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$unitPrice = (float) $product->price + (float) ($product->price_profit ?? 0);
|
||||
$amount = (float) ($product->amount ?? 1);
|
||||
|
||||
return $amount > 0 ? $unitPrice * $amount : $unitPrice;
|
||||
}
|
||||
|
||||
private function lineProduct(mixed $line): ?object
|
||||
{
|
||||
if (is_object($line) && isset($line->product)) {
|
||||
return $line->product;
|
||||
}
|
||||
|
||||
if (is_array($line) && isset($line['product'])) {
|
||||
$product = $line['product'];
|
||||
|
||||
return is_object($product) ? $product : (object) $product;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function walletLineGoldGrams(mixed $line): float
|
||||
{
|
||||
$gold = (float) ($this->lineValue($line, 'gold') ?? 0);
|
||||
if ($gold <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$product = $this->lineProduct($line);
|
||||
if ($product !== null) {
|
||||
return Controller::productLineWeightToGrams($product, $gold);
|
||||
}
|
||||
|
||||
return Controller::weightToGrams($gold);
|
||||
}
|
||||
|
||||
/** @param Collection|array<int, mixed>|null $wallets */
|
||||
private function mainWalletLine(Collection|array|null $wallets): mixed
|
||||
{
|
||||
|
||||
@@ -84,11 +84,7 @@ class WebsocketBroadcastService
|
||||
try {
|
||||
$response = Http::timeout(3)
|
||||
->withHeaders(['Content-Type' => 'application/json'])
|
||||
->withOptions([
|
||||
'curl' => [
|
||||
CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
|
||||
],
|
||||
])
|
||||
->withOptions(\App\Http\Controllers\Controller::httpClientOptions())
|
||||
->post($url, $body);
|
||||
|
||||
if (! $response->successful()) {
|
||||
|
||||
@@ -221,9 +221,7 @@ class ZaarboxPriceService
|
||||
return $empty;
|
||||
}
|
||||
|
||||
$base = (float) ($row['price'] ?? 0);
|
||||
$profit = (float) ($row['profit'] ?? 0);
|
||||
$orderPrice = (int) round($base + $profit);
|
||||
$orderPrice = $this->resolveReferenceProviderTotal($row);
|
||||
|
||||
return ['price' => $orderPrice, 'order_price' => $orderPrice, 'raw' => $row];
|
||||
} catch (\Throwable $e) {
|
||||
@@ -339,7 +337,7 @@ class ZaarboxPriceService
|
||||
return $empty;
|
||||
}
|
||||
|
||||
$orderPrice = (int) round((float) ($row['price'] ?? 0) + (float) ($row['profit'] ?? 0));
|
||||
$orderPrice = $this->resolveReferenceProviderTotal($row);
|
||||
|
||||
return ['order_price' => $orderPrice, 'raw' => $row];
|
||||
} catch (\Throwable $e) {
|
||||
@@ -386,23 +384,24 @@ class ZaarboxPriceService
|
||||
}
|
||||
|
||||
$row = $this->normalizeFeedRow($RPrice);
|
||||
$parts = $this->resolveReferenceBaseProfit($row);
|
||||
|
||||
if (($row['name'] ?? '') === 'TGJU فلز مس') {
|
||||
$copper = ($row['price'] ?? 0) + ($row['profit'] ?? 0);
|
||||
$copper = $parts['price'] + $parts['profit'];
|
||||
}
|
||||
if (($row['name'] ?? '') === 'TGJU دلار') {
|
||||
$dollar = ($row['price'] ?? 0) + ($row['profit'] ?? 0);
|
||||
$dollar = $parts['price'] + $parts['profit'];
|
||||
}
|
||||
|
||||
ProductPriceService::syncFromReference(
|
||||
$product,
|
||||
(int) $row['price'],
|
||||
(int) ($row['profit'] ?? 0)
|
||||
$parts['price'],
|
||||
$parts['profit']
|
||||
);
|
||||
|
||||
$productPrice = (float) ProductPriceService::adjustedTotal(
|
||||
(int) $row['price'],
|
||||
(int) ($row['profit'] ?? 0),
|
||||
$parts['price'],
|
||||
$parts['profit'],
|
||||
ProductPriceService::offsetPercent($product)
|
||||
);
|
||||
|
||||
@@ -477,22 +476,6 @@ class ZaarboxPriceService
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* قیمت نمایشی محصول از یک ردیف فید + offset درصد — بدون نوشتن DB
|
||||
*
|
||||
* @param array<string, mixed> $row
|
||||
*/
|
||||
public function computeAdjustedProductPrice(Product $product, array $row): int
|
||||
{
|
||||
$normalized = $this->normalizeFeedRow($row);
|
||||
|
||||
return ProductPriceService::adjustedTotal(
|
||||
(int) ($normalized['price'] ?? 0),
|
||||
(int) ($normalized['profit'] ?? 0),
|
||||
ProductPriceService::offsetPercent($product)
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $row */
|
||||
public function normalizeFeedRow(array $row): array
|
||||
{
|
||||
@@ -503,6 +486,55 @@ class ZaarboxPriceService
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* base/profit مرجع zaarbox — وقتی price صفر است provider قیمت نداده (profit مقدار قدیمی است).
|
||||
*
|
||||
* @param array<string, mixed> $row
|
||||
* @return array{price: int, profit: int}
|
||||
*/
|
||||
public function resolveReferenceBaseProfit(array $row): array
|
||||
{
|
||||
$normalized = $this->normalizeFeedRow($row);
|
||||
$price = (int) round((float) ($normalized['price'] ?? 0));
|
||||
if ($price <= 0) {
|
||||
return ['price' => 0, 'profit' => 0];
|
||||
}
|
||||
|
||||
return [
|
||||
'price' => $price,
|
||||
'profit' => (int) round((float) ($normalized['profit'] ?? 0)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
*/
|
||||
public function resolveReferenceProviderTotal(array $row): int
|
||||
{
|
||||
$parts = $this->resolveReferenceBaseProfit($row);
|
||||
|
||||
return $parts['price'] + $parts['profit'];
|
||||
}
|
||||
|
||||
/**
|
||||
* قیمت نمایشی محصول از یک ردیف فید + offset درصد — بدون نوشتن DB
|
||||
*
|
||||
* @param array<string, mixed> $row
|
||||
*/
|
||||
public function computeAdjustedProductPrice(Product $product, array $row): int
|
||||
{
|
||||
$parts = $this->resolveReferenceBaseProfit($row);
|
||||
if ($parts['price'] <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ProductPriceService::adjustedTotal(
|
||||
$parts['price'],
|
||||
$parts['profit'],
|
||||
ProductPriceService::offsetPercent($product)
|
||||
);
|
||||
}
|
||||
|
||||
public function syncProductFromApi(Product $product): bool
|
||||
{
|
||||
if (!Product::hasStoredPriceColumns()) {
|
||||
@@ -524,11 +556,12 @@ class ZaarboxPriceService
|
||||
}
|
||||
|
||||
$row = $this->normalizeFeedRow($RPrice);
|
||||
$parts = $this->resolveReferenceBaseProfit($row);
|
||||
|
||||
ProductPriceService::syncFromReference(
|
||||
$product,
|
||||
(int) $row['price'],
|
||||
(int) ($row['profit'] ?? 0)
|
||||
$parts['price'],
|
||||
$parts['profit']
|
||||
);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -371,6 +371,10 @@ INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `de
|
||||
SELECT 'metal_type', 'طلای آبشده', 'عنوان فلز در UI', 0, 1, NULL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'metal_type' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'main_metal', 'طلا', 'نام فلز اصلی (ادمین)', 0, 1, NULL, 'text'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'main_metal' LIMIT 1);
|
||||
|
||||
-- درگاه — کلیدهای اختیاری payping
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`)
|
||||
SELECT 'payping_fin_sync_enabled', 'false', 'همگامسازی fin', 1, 1, NULL
|
||||
@@ -425,19 +429,19 @@ SELECT 'referral_id', 'required', 'کد معرف اجباری', 1, 1, 'روشن
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'referral_id' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'fee_single', '500000', 'اختلاف پایه نمایش', 1, 1, NULL, 'number'
|
||||
SELECT 'fee_single', '500000', 'اختلاف پایه نمایش', 1, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_single' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'buy_fee', '', 'سود خرید نمایش', 1, 1, NULL, 'number'
|
||||
SELECT 'buy_fee', '', 'سود خرید نمایش', 1, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'buy_fee' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'sell_fee', '', 'سود فروش نمایش', 1, 1, NULL, 'number'
|
||||
SELECT 'sell_fee', '', 'سود فروش نمایش', 1, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'sell_fee' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'fee_cash', '500000', 'کارمزد ریالی سفارش (legacy)', 0, 1, NULL, 'number'
|
||||
SELECT 'fee_cash', '500000', 'کارمزد ریالی سفارش (legacy)', 0, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_cash' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
@@ -445,15 +449,15 @@ SELECT 'fee_service_charge_percentage', '0.005', 'درصد کارمزد سفار
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_service_charge_percentage' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'number'
|
||||
SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_withdraw' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'min_first_buy', '', 'حداقل اولین خرید (ریال)', 1, 1, NULL, 'number'
|
||||
SELECT 'min_first_buy', '', 'حداقل اولین خرید (ریال)', 1, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_first_buy' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'minimum_sell', '', 'حداقل فروش', 1, 1, NULL, 'number'
|
||||
SELECT 'minimum_sell', '', 'حداقل فروش', 1, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'minimum_sell' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
@@ -639,12 +643,18 @@ UPDATE `settings` SET `type` = 'switch' WHERE `key` IN (
|
||||
'banner_enabled'
|
||||
);
|
||||
|
||||
-- number
|
||||
UPDATE `settings` SET `type` = 'number' WHERE `key` IN (
|
||||
'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', 'fee_service_charge_percentage', 'fee_free',
|
||||
-- price (مبالغ ریالی)
|
||||
UPDATE `settings` SET `type` = 'price' WHERE `key` IN (
|
||||
'fee_single', 'buy_fee', 'sell_fee', 'fee_cash',
|
||||
'min_withdraw', 'min_first_buy', 'minimum_sell',
|
||||
'montzar_product_id', 'gift_for_referrer',
|
||||
'gift_for_referral', 'up_price', 'down_price', 'payping_fin_customer_id', 'tahesab_port',
|
||||
'up_price', 'down_price'
|
||||
);
|
||||
|
||||
-- number (شماره، شناسه، درصد، تعداد — بدون جداکننده هزارگان)
|
||||
UPDATE `settings` SET `type` = 'number' WHERE `key` IN (
|
||||
'fee_service_charge_percentage', 'fee_free', 'service_charge_percentage',
|
||||
'montzar_product_id', 'gift_for_referrer', 'gift_for_referral',
|
||||
'payping_fin_customer_id', 'tahesab_port', 'referral_limit',
|
||||
'deposit_sms_support_number'
|
||||
);
|
||||
|
||||
@@ -659,8 +669,8 @@ UPDATE `settings` SET `type` = 'text' WHERE `key` IN (
|
||||
'payping_fin_token', 'payping_gateway_item', 'tahesab_host', 'tahesab_token', 'tahesab_db',
|
||||
'node_client_id', 'node_shared_secret',
|
||||
'sse_path',
|
||||
'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', 'referral_limit',
|
||||
'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'montify_name', 'price_extreme_date',
|
||||
'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id',
|
||||
'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'main_metal', 'montify_name', 'price_extreme_date',
|
||||
'default_vc_number', 'liman_token'
|
||||
);
|
||||
|
||||
@@ -916,6 +926,7 @@ UPDATE `settings` SET `description` = 'آدرس تصویر لوگو در صفح
|
||||
UPDATE `settings` SET `description` = 'عنوان کوتاه برای نصب PWA روی موبایل. از دیپلوی مدیریت میشود.' WHERE `key` = 'title_pwa';
|
||||
UPDATE `settings` SET `description` = 'عنوان نمایشدادهشده در تب مرورگر (favicon). از دیپلوی مدیریت میشود.' WHERE `key` = 'title_fav';
|
||||
UPDATE `settings` SET `description` = 'عنوان فلز اصلی در رابط کاربر (مثلاً «طلای آبشده» یا «کاتد مس»). در دارایی، کیف پول و برچسبهای معامله دیده میشود. فقط از دیپلوی/SQL تغییر میکند.' WHERE `key` = 'metal_type';
|
||||
UPDATE `settings` SET `title_fa` = 'نام فلز اصلی (ادمین)', `description` = 'نام کوتاه فلز برای برچسبهای پنل ادمین (مثلاً «مس» یا «طلا»). «تراز طلایی» میشود «تراز مس» — نه «تراز مسی». فقط از دیپلوی/SQL تغییر میکند.' WHERE `key` = 'main_metal';
|
||||
UPDATE `settings` SET `description` = 'واحد نمایش وزن در داراییها، کیف پول و جزئیات معاملات|گرم,کیلوگرم' WHERE `key` = 'unit';
|
||||
UPDATE `settings` SET `description` = 'روشن: کاربران جدید میتوانند ثبتنام کنند. خاموش: فقط ورود کاربران موجود مجاز است.' WHERE `key` = 'open_register';
|
||||
UPDATE `settings` SET `description` = 'روشن: حساب کاربر بلافاصله پس از ثبتنام فعال میشود. خاموش: نیاز به تأیید دستی ادمین دارد.' WHERE `key` = 'auto_active_customer';
|
||||
@@ -1060,7 +1071,7 @@ UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN (
|
||||
'has_page_earn_money',
|
||||
'is_store',
|
||||
'logo_login', 'logo_register', 'title_pwa', 'title_fav',
|
||||
'main_product', 'main_wallet_products', 'metal_type',
|
||||
'main_product', 'main_wallet_products', 'metal_type', 'main_metal',
|
||||
'montify_name',
|
||||
'montzar_base_url', 'montzar_closed',
|
||||
'montzar_diff', 'montzar_last_product_log', 'montzar_live_price', 'montzar_mazaneh',
|
||||
@@ -1104,6 +1115,19 @@ UPDATE `settings` SET `can_edit` = 1 WHERE `key` IN (
|
||||
'text_about', 'text_rule'
|
||||
);
|
||||
|
||||
-- تفکیک نهایی type (بعد از همه batchها — override روی دیتابیس موجود)
|
||||
UPDATE `settings` SET `type` = 'price' WHERE `key` IN (
|
||||
'fee_single', 'buy_fee', 'sell_fee', 'fee_cash',
|
||||
'min_withdraw', 'min_first_buy', 'minimum_sell',
|
||||
'up_price', 'down_price'
|
||||
);
|
||||
UPDATE `settings` SET `type` = 'number' WHERE `key` IN (
|
||||
'fee_service_charge_percentage', 'fee_free', 'service_charge_percentage',
|
||||
'montzar_product_id', 'gift_for_referrer', 'gift_for_referral',
|
||||
'payping_fin_customer_id', 'tahesab_port', 'referral_limit',
|
||||
'deposit_sms_support_number'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT 'deploy_settings_const.sql finished' AS status;
|
||||
|
||||
@@ -351,6 +351,10 @@ INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `de
|
||||
SELECT 'metal_type', 'طلای آبشده', 'عنوان فلز در UI', 0, 1, NULL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'metal_type' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'main_metal', 'طلا', 'نام فلز اصلی (ادمین)', 0, 1, NULL, 'text'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'main_metal' LIMIT 1);
|
||||
|
||||
-- درگاه — کلیدهای اختیاری payping
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`)
|
||||
SELECT 'payping_fin_sync_enabled', 'false', 'همگامسازی fin', 1, 1, NULL
|
||||
@@ -405,19 +409,19 @@ SELECT 'referral_id', 'required', 'کد معرف اجباری', 1, 1, 'روشن
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'referral_id' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'fee_single', '500000', 'اختلاف پایه نمایش', 1, 1, NULL, 'number'
|
||||
SELECT 'fee_single', '500000', 'اختلاف پایه نمایش', 1, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_single' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'buy_fee', '', 'سود خرید نمایش', 1, 1, NULL, 'number'
|
||||
SELECT 'buy_fee', '', 'سود خرید نمایش', 1, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'buy_fee' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'sell_fee', '', 'سود فروش نمایش', 1, 1, NULL, 'number'
|
||||
SELECT 'sell_fee', '', 'سود فروش نمایش', 1, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'sell_fee' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'fee_cash', '500000', 'کارمزد ریالی سفارش (legacy)', 0, 1, NULL, 'number'
|
||||
SELECT 'fee_cash', '500000', 'کارمزد ریالی سفارش (legacy)', 0, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_cash' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
@@ -425,15 +429,15 @@ SELECT 'fee_service_charge_percentage', '0.005', 'درصد کارمزد سفار
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_service_charge_percentage' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'number'
|
||||
SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_withdraw' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'min_first_buy', '', 'حداقل اولین خرید (ریال)', 1, 1, NULL, 'number'
|
||||
SELECT 'min_first_buy', '', 'حداقل اولین خرید (ریال)', 1, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_first_buy' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'minimum_sell', '', 'حداقل فروش', 1, 1, NULL, 'number'
|
||||
SELECT 'minimum_sell', '', 'حداقل فروش', 1, 1, NULL, 'price'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'minimum_sell' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
@@ -615,12 +619,18 @@ UPDATE `settings` SET `type` = 'switch' WHERE `key` IN (
|
||||
'register_referral_sms_to_referrer', 'register_referral_bonus_message', 'price_from_nasiryan'
|
||||
);
|
||||
|
||||
-- number
|
||||
UPDATE `settings` SET `type` = 'number' WHERE `key` IN (
|
||||
'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', 'fee_service_charge_percentage', 'fee_free',
|
||||
-- price (مبالغ ریالی)
|
||||
UPDATE `settings` SET `type` = 'price' WHERE `key` IN (
|
||||
'fee_single', 'buy_fee', 'sell_fee', 'fee_cash',
|
||||
'min_withdraw', 'min_first_buy', 'minimum_sell',
|
||||
'montzar_product_id', 'gift_for_referrer',
|
||||
'gift_for_referral', 'up_price', 'down_price', 'payping_fin_customer_id', 'tahesab_port',
|
||||
'up_price', 'down_price'
|
||||
);
|
||||
|
||||
-- number (شماره، شناسه، درصد، تعداد — بدون جداکننده هزارگان)
|
||||
UPDATE `settings` SET `type` = 'number' WHERE `key` IN (
|
||||
'fee_service_charge_percentage', 'fee_free', 'service_charge_percentage',
|
||||
'montzar_product_id', 'gift_for_referrer', 'gift_for_referral',
|
||||
'payping_fin_customer_id', 'tahesab_port', 'referral_limit',
|
||||
'deposit_sms_support_number'
|
||||
);
|
||||
|
||||
@@ -635,8 +645,8 @@ UPDATE `settings` SET `type` = 'text' WHERE `key` IN (
|
||||
'payping_fin_token', 'payping_gateway_item', 'tahesab_host', 'tahesab_token', 'tahesab_db',
|
||||
'node_client_id', 'node_shared_secret',
|
||||
'sse_path',
|
||||
'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', 'referral_limit',
|
||||
'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'montify_name', 'price_extreme_date',
|
||||
'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id',
|
||||
'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'main_metal', 'montify_name', 'price_extreme_date',
|
||||
'default_vc_number', 'liman_token'
|
||||
);
|
||||
|
||||
@@ -920,6 +930,7 @@ UPDATE `settings` SET `description` = 'آدرس تصویر لوگو در صفح
|
||||
UPDATE `settings` SET `description` = 'عنوان کوتاه برای نصب PWA روی موبایل. از دیپلوی مدیریت میشود.' WHERE `key` = 'title_pwa';
|
||||
UPDATE `settings` SET `description` = 'عنوان نمایشدادهشده در تب مرورگر (favicon). از دیپلوی مدیریت میشود.' WHERE `key` = 'title_fav';
|
||||
UPDATE `settings` SET `description` = 'عنوان فلز اصلی در رابط کاربر (مثلاً «طلای آبشده» یا «کاتد مس»). در دارایی، کیف پول و برچسبهای معامله دیده میشود. فقط از دیپلوی/SQL تغییر میکند.' WHERE `key` = 'metal_type';
|
||||
UPDATE `settings` SET `title_fa` = 'نام فلز اصلی (ادمین)', `description` = 'نام کوتاه فلز برای برچسبهای پنل ادمین (مثلاً «مس» یا «طلا»). «تراز طلایی» میشود «تراز مس» — نه «تراز مسی». فقط از دیپلوی/SQL تغییر میکند.' WHERE `key` = 'main_metal';
|
||||
UPDATE `settings` SET `description` = 'واحد نمایش وزن در داراییها، کیف پول و جزئیات معاملات|گرم,کیلوگرم' WHERE `key` = 'unit';
|
||||
UPDATE `settings` SET `description` = 'روشن: کاربران جدید میتوانند ثبتنام کنند. خاموش: فقط ورود کاربران موجود مجاز است.' WHERE `key` = 'open_register';
|
||||
UPDATE `settings` SET `description` = 'روشن: حساب کاربر بلافاصله پس از ثبتنام فعال میشود. خاموش: نیاز به تأیید دستی ادمین دارد.' WHERE `key` = 'auto_active_customer';
|
||||
@@ -1158,6 +1169,19 @@ JOIN (
|
||||
) d ON d.`key` = s.`key`
|
||||
SET s.`can_edit` = d.`can_edit`;
|
||||
|
||||
-- تفکیک نهایی type (بعد از همه batchها — override روی دیتابیس موجود)
|
||||
UPDATE `settings` SET `type` = 'price' WHERE `key` IN (
|
||||
'fee_single', 'buy_fee', 'sell_fee', 'fee_cash',
|
||||
'min_withdraw', 'min_first_buy', 'minimum_sell',
|
||||
'up_price', 'down_price'
|
||||
);
|
||||
UPDATE `settings` SET `type` = 'number' WHERE `key` IN (
|
||||
'fee_service_charge_percentage', 'fee_free', 'service_charge_percentage',
|
||||
'montzar_product_id', 'gift_for_referrer', 'gift_for_referral',
|
||||
'payping_fin_customer_id', 'tahesab_port', 'referral_limit',
|
||||
'deposit_sms_support_number'
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
|
||||
SELECT 'deploy_settings_const.sql finished' AS status;
|
||||
|
||||
@@ -43,7 +43,16 @@ class Transaction extends Model
|
||||
}
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo('App\Models\Product','product_id')->select(['id','title','group_type']);
|
||||
return $this->belongsTo('App\Models\Product', 'product_id')->select([
|
||||
'id',
|
||||
'title',
|
||||
'group_type',
|
||||
'unit',
|
||||
'direct_price',
|
||||
'amount',
|
||||
'trade_by_quantity',
|
||||
'metal_id',
|
||||
]);
|
||||
}
|
||||
public function order()
|
||||
{
|
||||
|
||||
@@ -20,9 +20,9 @@ class Wallet extends Model
|
||||
{
|
||||
return $this->belongsTo('App\Models\Fund','fund_id')->select(['id','title']);
|
||||
}
|
||||
public function Product()
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo('App\Models\Product','product_id');
|
||||
return $this->belongsTo('App\Models\Product', 'product_id');
|
||||
}
|
||||
|
||||
/** کیف اصلی مشتری — بدون fund و بدون product */
|
||||
|
||||
@@ -104,6 +104,8 @@ Route::middleware('auth:user')->group(function (){
|
||||
//-------------wallet
|
||||
Route::post('/wallet/charge',[WalletController::class,'charge']);
|
||||
Route::post('/wallet/charge_gold',[WalletController::class,'charge_gold']);
|
||||
Route::post('/wallet/charge_wallet',[WalletController::class,'charge_wallet']);
|
||||
Route::get('/wallet/charge-targets',[WalletController::class,'chargeTargets']);
|
||||
Route::post('/wallet/table',[WalletController::class,'indexWallet']);
|
||||
Route::post('/wallet/transaction/table',[WalletController::class,'index']);
|
||||
Route::post('/wallet/transaction/update',[WalletController::class,'updateTransaction']);
|
||||
|
||||
Reference in New Issue
Block a user