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:
2026-06-24 01:22:56 +03:30
co-authored by Cursor
24 changed files with 1762 additions and 375 deletions
@@ -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');
+13 -9
View File
@@ -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();
+37 -2
View File
@@ -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;
}
}
+190 -9
View File
@@ -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);
+125 -15
View File
@@ -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;
}