fix some bugs 2
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\DB\Te\TableEngine;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ConditionalOrder;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Product;
|
||||
use App\Services\ConditionalOrderService;
|
||||
use App\Services\ConfigService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
|
||||
class ConditionalOrderController extends Controller
|
||||
{
|
||||
public function table(Request $request)
|
||||
{
|
||||
$query = ConditionalOrder::query()->with(['customer', 'product']);
|
||||
|
||||
TableEngine::create($query, $request->all())
|
||||
->initSearch(['customer_id', 'product_id', 'status', 'direction', 'price_condition'])
|
||||
->select(['*'])
|
||||
->export($query);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$data = $this->validatedPayload($request);
|
||||
$customer = Customer::findOrFail($data['customer_id']);
|
||||
|
||||
$service = app(ConditionalOrderService::class);
|
||||
if (! app(ConfigService::class)->isConditionalOrderEnabled()) {
|
||||
throw new UnprocessableEntityHttpException('قابلیت سفارش شرطی سراسری خاموش است.');
|
||||
}
|
||||
if (! $customer->conditional_order_active) {
|
||||
throw new UnprocessableEntityHttpException('سفارش شرطی برای این مشتری مجاز نیست.');
|
||||
}
|
||||
$service->assertWithinLimit($customer);
|
||||
|
||||
$fee = 0;
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$fee = $service->chargeRegistrationFee($customer);
|
||||
$order = ConditionalOrder::create([
|
||||
...$data,
|
||||
'is_active' => $data['is_active'] ?? true,
|
||||
'status' => ConditionalOrder::STATUS_PENDING,
|
||||
'fee_charged' => $fee,
|
||||
]);
|
||||
DB::commit();
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return response()->json($order->load(['customer', 'product']), 201);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id)
|
||||
{
|
||||
$order = ConditionalOrder::findOrFail($id);
|
||||
$data = $this->validatedPayload($request, true);
|
||||
unset($data['customer_id']);
|
||||
$order->update($data);
|
||||
|
||||
return response()->json(true);
|
||||
}
|
||||
|
||||
public function toggleActive(int $id)
|
||||
{
|
||||
$order = ConditionalOrder::findOrFail($id);
|
||||
if ($order->status !== ConditionalOrder::STATUS_PENDING) {
|
||||
throw new UnprocessableEntityHttpException('فقط سفارشهای در انتظار قابل فعال/غیرفعالسازی هستند.');
|
||||
}
|
||||
|
||||
$order->is_active = ! $order->is_active;
|
||||
$order->save();
|
||||
|
||||
return response()->json(['is_active' => (bool) $order->is_active]);
|
||||
}
|
||||
|
||||
public function destroy(int $id)
|
||||
{
|
||||
ConditionalOrder::findOrFail($id)->delete();
|
||||
|
||||
return response()->json(true);
|
||||
}
|
||||
|
||||
private function validatedPayload(Request $request, bool $partial = false): array
|
||||
{
|
||||
$rules = [
|
||||
'customer_id' => ($partial ? 'sometimes|' : '') . 'required|integer|exists:customers,id',
|
||||
'product_id' => ($partial ? 'sometimes|' : '') . 'required|integer|exists:products,id',
|
||||
'direction' => ($partial ? 'sometimes|' : '') . 'required|integer|in:1,2',
|
||||
'price_condition' => ($partial ? 'sometimes|' : '') . 'required|in:gte,lte',
|
||||
'trigger_price' => ($partial ? 'sometimes|' : '') . 'required|numeric|min:1',
|
||||
'amount' => 'nullable|numeric|min:0',
|
||||
'number' => 'nullable|numeric|min:0',
|
||||
'expires_at' => 'nullable|date',
|
||||
'description' => 'nullable|string|max:2000',
|
||||
'is_active' => 'sometimes|boolean',
|
||||
];
|
||||
|
||||
$data = $request->validate($rules);
|
||||
|
||||
$productId = (int) ($data['product_id'] ?? $request->input('product_id') ?? 0);
|
||||
$product = $productId > 0 ? Product::find($productId) : null;
|
||||
|
||||
$amount = isset($data['amount']) ? (float) $data['amount'] : null;
|
||||
$number = isset($data['number']) ? (float) $data['number'] : null;
|
||||
|
||||
if (! $partial) {
|
||||
if (($amount === null || $amount <= 0) && ($number === null || $number <= 0)) {
|
||||
throw new UnprocessableEntityHttpException('مقدار یا تعداد را وارد کنید.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($product && $amount !== null && $amount > 0 && ($number === null || $number <= 0)) {
|
||||
$unit = (float) ($product->amount ?? 0);
|
||||
if ($unit > 0 && ! in_array(trim((string) ($product->group_type ?? '')), ['آبشده', 'کاتد مس'], true)
|
||||
&& ! (bool) ($product->trade_by_quantity ?? false)) {
|
||||
$data['number'] = $amount;
|
||||
$data['amount'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -266,6 +266,34 @@ class CustomerController extends SuperController
|
||||
]);
|
||||
return response()->json(true);
|
||||
}
|
||||
|
||||
public function changeConditionalOrderActive($id)
|
||||
{
|
||||
$query = Customer::findOrFail($id);
|
||||
$query->update([
|
||||
'conditional_order_active' => ! $query->conditional_order_active,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'conditional_order_active' => (bool) $query->fresh()->conditional_order_active,
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateConditionalOrderLimit(Request $request, $id)
|
||||
{
|
||||
$query = Customer::findOrFail($id);
|
||||
$data = $request->validate([
|
||||
'conditional_order_limit' => 'nullable|integer|min:1',
|
||||
]);
|
||||
|
||||
$query->update([
|
||||
'conditional_order_limit' => $data['conditional_order_limit'] ?? null,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'conditional_order_limit' => $query->fresh()->conditional_order_limit,
|
||||
]);
|
||||
}
|
||||
// single customer-------------------
|
||||
|
||||
public function singleArchiveTable(Request $request)
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Customer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ConditionalOrder;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Product;
|
||||
use App\Services\ConditionalOrderService;
|
||||
use App\Services\ConfigService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
|
||||
class ConditionalOrderController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$customer = $this->authCustomer();
|
||||
app(ConditionalOrderService::class)->assertCustomerCanUse($customer);
|
||||
|
||||
$items = ConditionalOrder::query()
|
||||
->with('product')
|
||||
->where('customer_id', $customer->id)
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
return response()->json($items);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$customer = $this->authCustomer();
|
||||
$service = app(ConditionalOrderService::class);
|
||||
$service->assertCustomerCanUse($customer);
|
||||
$service->assertWithinLimit($customer);
|
||||
|
||||
$data = $this->validatedPayload($request);
|
||||
$data['customer_id'] = $customer->id;
|
||||
|
||||
$fee = 0;
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$fee = $service->chargeRegistrationFee($customer);
|
||||
$order = ConditionalOrder::create([
|
||||
...$data,
|
||||
'is_active' => $data['is_active'] ?? true,
|
||||
'status' => ConditionalOrder::STATUS_PENDING,
|
||||
'fee_charged' => $fee,
|
||||
]);
|
||||
DB::commit();
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return response()->json($order->load('product'), 201);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id)
|
||||
{
|
||||
$customer = $this->authCustomer();
|
||||
app(ConditionalOrderService::class)->assertCustomerCanUse($customer);
|
||||
|
||||
$order = ConditionalOrder::query()
|
||||
->where('customer_id', $customer->id)
|
||||
->findOrFail($id);
|
||||
|
||||
if ($order->status !== ConditionalOrder::STATUS_PENDING) {
|
||||
throw new UnprocessableEntityHttpException('فقط سفارش در انتظار قابل ویرایش است.');
|
||||
}
|
||||
|
||||
$data = $this->validatedPayload($request, true);
|
||||
unset($data['customer_id']);
|
||||
$order->update($data);
|
||||
|
||||
return response()->json($order->fresh()->load('product'));
|
||||
}
|
||||
|
||||
public function destroy(int $id)
|
||||
{
|
||||
$customer = $this->authCustomer();
|
||||
app(ConditionalOrderService::class)->assertCustomerCanUse($customer);
|
||||
|
||||
$order = ConditionalOrder::query()
|
||||
->where('customer_id', $customer->id)
|
||||
->findOrFail($id);
|
||||
$order->delete();
|
||||
|
||||
return response()->json(true);
|
||||
}
|
||||
|
||||
public function toggleActive(int $id)
|
||||
{
|
||||
$customer = $this->authCustomer();
|
||||
app(ConditionalOrderService::class)->assertCustomerCanUse($customer);
|
||||
|
||||
$order = ConditionalOrder::query()
|
||||
->where('customer_id', $customer->id)
|
||||
->findOrFail($id);
|
||||
|
||||
if ($order->status !== ConditionalOrder::STATUS_PENDING) {
|
||||
throw new UnprocessableEntityHttpException('فقط سفارش در انتظار قابل فعال/غیرفعالسازی است.');
|
||||
}
|
||||
|
||||
$order->is_active = ! $order->is_active;
|
||||
$order->save();
|
||||
|
||||
return response()->json(['is_active' => (bool) $order->is_active]);
|
||||
}
|
||||
|
||||
private function authCustomer(): Customer
|
||||
{
|
||||
$customer = auth('customer')->user();
|
||||
if (! $customer) {
|
||||
throw new UnprocessableEntityHttpException('احراز هویت نامعتبر است.');
|
||||
}
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
private function validatedPayload(Request $request, bool $partial = false): array
|
||||
{
|
||||
$rules = [
|
||||
'product_id' => ($partial ? 'sometimes|' : '') . 'required|integer|exists:products,id',
|
||||
'direction' => ($partial ? 'sometimes|' : '') . 'required|integer|in:1,2',
|
||||
'price_condition' => ($partial ? 'sometimes|' : '') . 'required|in:gte,lte',
|
||||
'trigger_price' => ($partial ? 'sometimes|' : '') . 'required|numeric|min:1',
|
||||
'amount' => 'nullable|numeric|min:0',
|
||||
'number' => 'nullable|numeric|min:0',
|
||||
'expires_at' => 'nullable|date|after:now',
|
||||
'description' => 'nullable|string|max:2000',
|
||||
'is_active' => 'sometimes|boolean',
|
||||
];
|
||||
|
||||
$data = $request->validate($rules);
|
||||
|
||||
$amount = isset($data['amount']) ? (float) $data['amount'] : null;
|
||||
$number = isset($data['number']) ? (float) $data['number'] : null;
|
||||
|
||||
if (! $partial && ($amount === null || $amount <= 0) && ($number === null || $number <= 0)) {
|
||||
throw new UnprocessableEntityHttpException('مقدار گرم یا تعداد را وارد کنید.');
|
||||
}
|
||||
|
||||
$productId = (int) ($data['product_id'] ?? $request->input('product_id') ?? 0);
|
||||
$product = $productId > 0 ? Product::find($productId) : null;
|
||||
if ($product && $amount !== null && $amount > 0 && ($number === null || $number <= 0)) {
|
||||
$group = trim((string) ($product->group_type ?? ''));
|
||||
$isMelted = in_array($group, ['آبشده', 'کاتد مس'], true)
|
||||
|| (bool) ($product->trade_by_quantity ?? false)
|
||||
|| ((int) $product->id === (int) app(ConfigService::class)->mainProductId());
|
||||
|
||||
if (! $isMelted && (float) ($product->amount ?? 0) > 0) {
|
||||
$data['number'] = $amount;
|
||||
$data['amount'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -157,6 +157,17 @@ class ReportController extends Controller
|
||||
})->afterResponse();
|
||||
}
|
||||
|
||||
if ($isFreshPrice && (int) ($payload['price'] ?? 0) > 0) {
|
||||
$pricePayload = $payload;
|
||||
if (app()->runningInConsole()) {
|
||||
app(\App\Services\ConditionalOrderService::class)->evaluateFromLastPrice($pricePayload);
|
||||
} else {
|
||||
dispatch(function () use ($pricePayload) {
|
||||
app(\App\Services\ConditionalOrderService::class)->evaluateFromLastPrice($pricePayload);
|
||||
})->afterResponse();
|
||||
}
|
||||
}
|
||||
|
||||
if ($isFreshPrice) {
|
||||
$cache = app(LivePriceCacheService::class);
|
||||
$cache->markTick();
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class ConditionalOrder extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
public const STATUS_PENDING = 'pending';
|
||||
public const STATUS_EXECUTED = 'executed';
|
||||
public const STATUS_FAILED = 'failed';
|
||||
public const STATUS_EXPIRED = 'expired';
|
||||
|
||||
public const CONDITION_GTE = 'gte';
|
||||
public const CONDITION_LTE = 'lte';
|
||||
|
||||
protected $fillable = [
|
||||
'customer_id',
|
||||
'product_id',
|
||||
'direction',
|
||||
'price_condition',
|
||||
'trigger_price',
|
||||
'amount',
|
||||
'number',
|
||||
'expires_at',
|
||||
'description',
|
||||
'is_active',
|
||||
'status',
|
||||
'fail_reason',
|
||||
'order_id',
|
||||
'fee_charged',
|
||||
'triggered_price',
|
||||
'executed_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'direction' => 'integer',
|
||||
'trigger_price' => 'integer',
|
||||
'amount' => 'float',
|
||||
'number' => 'float',
|
||||
'fee_charged' => 'integer',
|
||||
'triggered_price' => 'integer',
|
||||
'expires_at' => 'datetime',
|
||||
'executed_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function customer()
|
||||
{
|
||||
return $this->belongsTo(Customer::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
|
||||
public function order()
|
||||
{
|
||||
return $this->belongsTo(Order::class);
|
||||
}
|
||||
|
||||
public function isPending(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_PENDING;
|
||||
}
|
||||
|
||||
public function conditionMet(int $currentPrice): bool
|
||||
{
|
||||
if ($currentPrice <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$target = (int) $this->trigger_price;
|
||||
|
||||
return match ($this->price_condition) {
|
||||
self::CONDITION_GTE => $currentPrice >= $target,
|
||||
self::CONDITION_LTE => $currentPrice <= $target,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,8 @@ class Customer extends Authenticatable implements JWTSubject
|
||||
'is_archive',
|
||||
'gram',
|
||||
'trade_active',//آیا امکان معامله دارد؟
|
||||
'conditional_order_active',//آیا سفارش شرطی مجاز است؟
|
||||
'conditional_order_limit',//سقف تعداد pending (null = نامحدود)
|
||||
'type',
|
||||
'referral_id',
|
||||
'referral_to_id',
|
||||
@@ -69,6 +71,8 @@ class Customer extends Authenticatable implements JWTSubject
|
||||
"gold" => "float",
|
||||
"is_archive" => "boolean",
|
||||
"finger_print" => "boolean",
|
||||
"conditional_order_active" => "boolean",
|
||||
"conditional_order_limit" => "integer",
|
||||
];
|
||||
static $TYPE = [
|
||||
1=>'همکار',
|
||||
|
||||
@@ -40,6 +40,17 @@ class AssetSummaryService
|
||||
|
||||
$mainWallet = $this->resolveMainWallet($userWallets);
|
||||
$metalIds = $this->catalogMetalIds($catalog);
|
||||
// موجودی کیف اصلی نباید بهخاطر inactive بودن محصول نماینده از metal_totals حذف شود
|
||||
if ($customerId > 0) {
|
||||
$effectiveGold = $this->walletBalanceService->effectiveMainWalletGold($customerId, $ownerType);
|
||||
if (abs($effectiveGold) >= 0.000001) {
|
||||
$mainMetalId = $this->walletBalanceService->resolveMainWalletMetalId();
|
||||
if ($mainMetalId > 0 && !in_array($mainMetalId, $metalIds, true)) {
|
||||
$metalIds[] = $mainMetalId;
|
||||
sort($metalIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
$weightUnit = Controller::tradeWeightUnit();
|
||||
$hasCountBased = $this->catalogHasCountBased($catalog);
|
||||
$hasWeightBased = $this->catalogHasWeightBased($catalog);
|
||||
@@ -478,7 +489,11 @@ class AssetSummaryService
|
||||
return $group !== '' ? $group : 'آبشده';
|
||||
}
|
||||
|
||||
/** اولین محصولِ لیست main_wallet_products که در کاتالوگ فعال است. */
|
||||
/**
|
||||
* اولین محصولِ لیست main_wallet_products.
|
||||
* اگر در کاتالوگ فیلترشده (مثلاً فقط active) نبود، از DB بدون فیلتر active میخواند
|
||||
* تا موجودی کیف اصلی برای اپ مشتری صفر نشود وقتی محصول نماینده غیرفعال است.
|
||||
*/
|
||||
private function resolveMainWalletRepresentative(Collection $catalogById, array $mainWalletProductIds): ?Product
|
||||
{
|
||||
foreach ($mainWalletProductIds as $productId) {
|
||||
@@ -488,6 +503,23 @@ class AssetSummaryService
|
||||
}
|
||||
}
|
||||
|
||||
if ($mainWalletProductIds === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$byId = Product::query()
|
||||
->with('file')
|
||||
->whereIn('id', $mainWalletProductIds)
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
foreach ($mainWalletProductIds as $productId) {
|
||||
$product = $byId->get((int) $productId);
|
||||
if ($product) {
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -668,7 +700,11 @@ class AssetSummaryService
|
||||
|
||||
if ($mainWalletProductIds !== []) {
|
||||
// حالت صریح: کیف اصلی فقط یکبار، زیر فلزِ محصول نماینده
|
||||
$meltedGrams = ($mainWalletMetalId !== null && (int) $metalId === $mainWalletMetalId)
|
||||
// اگر نماینده پیدا نشد (محصول حذفشده)، به فلز resolveMainWalletMetalId برگرد
|
||||
if ($mainWalletMetalId === null || $mainWalletMetalId <= 0) {
|
||||
$mainWalletMetalId = $this->walletBalanceService->resolveMainWalletMetalId();
|
||||
}
|
||||
$meltedGrams = ((int) $metalId === (int) $mainWalletMetalId)
|
||||
? $mainWalletGrams
|
||||
: 0.0;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\Single\SingleOrderController;
|
||||
use App\Models\ConditionalOrder;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
|
||||
|
||||
class ConditionalOrderService
|
||||
{
|
||||
public function evaluateFromLastPrice(array $payload = []): void
|
||||
{
|
||||
$config = app(ConfigService::class);
|
||||
if (! $config->isConditionalOrderEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->expirePendingPastDue();
|
||||
|
||||
$orders = ConditionalOrder::query()
|
||||
->with(['customer', 'product'])
|
||||
->where('status', ConditionalOrder::STATUS_PENDING)
|
||||
->where('is_active', true)
|
||||
->whereHas('customer', function ($q) {
|
||||
$q->where('conditional_order_active', 1);
|
||||
})
|
||||
->get();
|
||||
|
||||
if ($orders->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$display = app(ProductDisplayPriceService::class);
|
||||
$liquidPrice = (int) ($payload['price'] ?? $payload['buy'] ?? 0);
|
||||
if ($liquidPrice <= 0) {
|
||||
$liquidPrice = (int) ($config->getInt('montzar_live_price', 0));
|
||||
}
|
||||
|
||||
$productsById = Product::query()
|
||||
->whereIn('id', $orders->pluck('product_id')->unique()->filter()->all())
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
if ($productsById->isNotEmpty()) {
|
||||
$display->enrichForApi($productsById);
|
||||
}
|
||||
|
||||
foreach ($orders as $order) {
|
||||
try {
|
||||
$this->tryExecute($order, $productsById, $liquidPrice, $config);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Conditional order evaluate failed', [
|
||||
'id' => $order->id,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function tryExecute(
|
||||
ConditionalOrder $order,
|
||||
$productsById,
|
||||
int $liquidPrice,
|
||||
ConfigService $config
|
||||
): void {
|
||||
$product = $productsById->get($order->product_id) ?? $order->product;
|
||||
if (! $product) {
|
||||
return;
|
||||
}
|
||||
|
||||
$display = app(ProductDisplayPriceService::class);
|
||||
$currentPrice = $this->resolveComparePrice($product, $liquidPrice, $display);
|
||||
if ($currentPrice <= 0 || ! $order->conditionMet($currentPrice)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$config->assertProductTradingOpen($product);
|
||||
} catch (\Throwable $e) {
|
||||
// خارج ساعت — pending بماند
|
||||
return;
|
||||
}
|
||||
|
||||
$this->executeOrder($order, $product, $currentPrice);
|
||||
}
|
||||
|
||||
private function resolveComparePrice(Product $product, int $liquidPrice, ProductDisplayPriceService $display): int
|
||||
{
|
||||
if ($display->usesAppPrice($product)) {
|
||||
$base = $liquidPrice > 0 ? $liquidPrice : $display->resolveOrderBasePrice($product);
|
||||
|
||||
return max(0, (int) $base);
|
||||
}
|
||||
|
||||
$price = (int) ($product->price ?? 0);
|
||||
if ($price > 0) {
|
||||
return $price;
|
||||
}
|
||||
|
||||
return max(0, (int) $display->resolveOrderBasePrice($product));
|
||||
}
|
||||
|
||||
private function executeOrder(ConditionalOrder $order, Product $product, int $currentPrice): void
|
||||
{
|
||||
$customer = $order->customer ?? Customer::find($order->customer_id);
|
||||
if (! $customer || ! $customer->conditional_order_active) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'customer_id' => (int) $customer->id,
|
||||
'direction' => (int) $order->direction,
|
||||
'method' => 'cash',
|
||||
];
|
||||
|
||||
if ($this->isMeltedLikeProduct($product)) {
|
||||
$payload['abshode_id'] = (int) $product->id;
|
||||
$payload['amount'] = (float) ($order->amount ?? 0);
|
||||
if ($payload['amount'] <= 0) {
|
||||
$this->markFailed($order, $currentPrice, 'مقدار گرم نامعتبر است');
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$payload['product_id'] = (int) $product->id;
|
||||
$payload['number'] = (float) ($order->number ?? $order->amount ?? 0);
|
||||
if ($payload['number'] <= 0) {
|
||||
$this->markFailed($order, $currentPrice, 'تعداد نامعتبر است');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$guard = Auth::guard('customer');
|
||||
$previous = $guard->user();
|
||||
$guard->setUser($customer);
|
||||
|
||||
try {
|
||||
$request = Request::create('/customer/single/order', 'POST', $payload);
|
||||
$response = app(SingleOrderController::class)->store($request);
|
||||
$body = method_exists($response, 'getData')
|
||||
? (array) $response->getData(true)
|
||||
: (is_array($response) ? $response : []);
|
||||
|
||||
$orderData = (isset($body[1]) && is_array($body[1])) ? $body[1] : $body;
|
||||
$createdOrderId = (int) (
|
||||
data_get($orderData, 'order.id')
|
||||
?: data_get($orderData, 'id')
|
||||
?: 0
|
||||
);
|
||||
|
||||
$order->status = ConditionalOrder::STATUS_EXECUTED;
|
||||
$order->is_active = false;
|
||||
$order->order_id = $createdOrderId > 0 ? $createdOrderId : null;
|
||||
$order->triggered_price = $currentPrice;
|
||||
$order->executed_at = now();
|
||||
$order->fail_reason = null;
|
||||
$order->save();
|
||||
} catch (\Throwable $e) {
|
||||
$message = $e instanceof HttpExceptionInterface
|
||||
? ($e->getMessage() ?: 'خطا در ثبت سفارش')
|
||||
: ($e->getMessage() ?: 'خطا در ثبت سفارش');
|
||||
|
||||
$this->markFailed($order, $currentPrice, $message);
|
||||
} finally {
|
||||
if ($previous) {
|
||||
$guard->setUser($previous);
|
||||
} else {
|
||||
$guard->forgetUser();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function markFailed(ConditionalOrder $order, int $currentPrice, string $reason): void
|
||||
{
|
||||
$order->status = ConditionalOrder::STATUS_FAILED;
|
||||
$order->is_active = false;
|
||||
$order->triggered_price = $currentPrice;
|
||||
$order->executed_at = now();
|
||||
$order->fail_reason = mb_substr($reason, 0, 250);
|
||||
$order->save();
|
||||
}
|
||||
|
||||
/** آبشده / معامله وزنی → amount؛ محصول ثابت → number */
|
||||
private function isMeltedLikeProduct(Product $product): bool
|
||||
{
|
||||
$group = trim((string) ($product->group_type ?? ''));
|
||||
if (in_array($group, ['آبشده', 'کاتد مس'], true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((bool) ($product->trade_by_quantity ?? false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$mainId = app(ConfigService::class)->mainProductId();
|
||||
|
||||
return $mainId > 0 && (int) $product->id === (int) $mainId;
|
||||
}
|
||||
|
||||
public function expirePendingPastDue(): int
|
||||
{
|
||||
return ConditionalOrder::query()
|
||||
->where('status', ConditionalOrder::STATUS_PENDING)
|
||||
->whereNotNull('expires_at')
|
||||
->where('expires_at', '<', now())
|
||||
->update([
|
||||
'status' => ConditionalOrder::STATUS_EXPIRED,
|
||||
'is_active' => false,
|
||||
'fail_reason' => 'منقضی شده',
|
||||
]);
|
||||
}
|
||||
|
||||
public function assertCustomerCanUse(Customer $customer): void
|
||||
{
|
||||
$config = app(ConfigService::class);
|
||||
if (! $config->isConditionalOrderEnabled()) {
|
||||
throw new \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException(
|
||||
'قابلیت سفارش شرطی غیرفعال است.'
|
||||
);
|
||||
}
|
||||
|
||||
if (! $customer->conditional_order_active) {
|
||||
throw new \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException(
|
||||
'سفارش شرطی برای شما مجاز نیست.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function assertWithinLimit(Customer $customer): void
|
||||
{
|
||||
$limit = $customer->conditional_order_limit;
|
||||
if ($limit === null || (int) $limit <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pending = ConditionalOrder::query()
|
||||
->where('customer_id', $customer->id)
|
||||
->where('status', ConditionalOrder::STATUS_PENDING)
|
||||
->count();
|
||||
|
||||
if ($pending >= (int) $limit) {
|
||||
throw new \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException(
|
||||
'سقف تعداد سفارش شرطی شما پر شده است.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function chargeRegistrationFee(Customer $customer): int
|
||||
{
|
||||
$fee = app(ConfigService::class)->conditionalOrderFee();
|
||||
if ($fee <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$walletInterface = new \Rooyesh\Wallet\interfaces\WalletInterface(
|
||||
(int) $customer->id,
|
||||
(int) $customer->type
|
||||
);
|
||||
$wallet = $walletInterface->wallet;
|
||||
if (! $wallet || (float) $wallet->cash < $fee) {
|
||||
throw new \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException(
|
||||
'موجودی ریالی برای کارمزد سفارش شرطی کافی نیست.'
|
||||
);
|
||||
}
|
||||
|
||||
new \Rooyesh\Wallet\interfaces\TransactionInterface($walletInterface, [
|
||||
'cash' => $fee,
|
||||
'expire_at' => now(),
|
||||
'note' => 'کارمزد ثبت سفارش شرطی',
|
||||
'title' => 'کارمزد سفارش شرطی',
|
||||
'active' => true,
|
||||
], 4, true, false);
|
||||
|
||||
return $fee;
|
||||
}
|
||||
}
|
||||
@@ -730,6 +730,17 @@ class ConfigService
|
||||
public const ACCOUNTING_PARGAR = 'pargar';
|
||||
|
||||
/** none | tahesab | pargar — از setting accounting_provider */
|
||||
public function isConditionalOrderEnabled(): bool
|
||||
{
|
||||
return $this->getBool('enable_conditional_order', false);
|
||||
}
|
||||
|
||||
/** کارمزد ریالی هر بار ثبت تریگر سفارش شرطی */
|
||||
public function conditionalOrderFee(): int
|
||||
{
|
||||
return max(0, $this->getInt('fee_conditional_order', 0));
|
||||
}
|
||||
|
||||
public function accountingProvider(): string
|
||||
{
|
||||
$provider = strtolower(trim($this->getString('accounting_provider', '')));
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('customers')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addCustomerColumnIfMissing(
|
||||
'conditional_order_active',
|
||||
'TINYINT(1) NOT NULL DEFAULT 1',
|
||||
'trade_active'
|
||||
);
|
||||
$this->addCustomerColumnIfMissing(
|
||||
'conditional_order_limit',
|
||||
'INT UNSIGNED NULL DEFAULT NULL',
|
||||
'conditional_order_active'
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// عمداً خالی — rollback ستون را حذف نمیکند
|
||||
}
|
||||
|
||||
private function customersTable(): string
|
||||
{
|
||||
return DB::getTablePrefix().'customers';
|
||||
}
|
||||
|
||||
private function customerColumnExists(string $column): bool
|
||||
{
|
||||
$result = DB::selectOne(
|
||||
'SELECT COUNT(*) AS cnt FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?',
|
||||
[DB::connection()->getDatabaseName(), $this->customersTable(), $column]
|
||||
);
|
||||
|
||||
return (int) $result->cnt > 0;
|
||||
}
|
||||
|
||||
private function addCustomerColumnIfMissing(string $column, string $definition, ?string $after = null): void
|
||||
{
|
||||
if ($this->customerColumnExists($column)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$table = $this->customersTable();
|
||||
$afterClause = ($after !== null && $this->customerColumnExists($after))
|
||||
? " AFTER `{$after}`"
|
||||
: '';
|
||||
|
||||
DB::statement("ALTER TABLE `{$table}` ADD COLUMN `{$column}` {$definition}{$afterClause}");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('conditional_orders')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('conditional_orders', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('customer_id')->index();
|
||||
$table->unsignedBigInteger('product_id')->index();
|
||||
$table->unsignedTinyInteger('direction'); // 1 buy, 2 sell
|
||||
$table->string('price_condition', 8); // gte | lte
|
||||
$table->unsignedBigInteger('trigger_price');
|
||||
$table->decimal('amount', 16, 4)->nullable();
|
||||
$table->decimal('number', 16, 4)->nullable();
|
||||
$table->timestamp('expires_at')->nullable()->index();
|
||||
$table->text('description')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->string('status', 16)->default('pending')->index(); // pending|executed|failed|expired
|
||||
$table->string('fail_reason')->nullable();
|
||||
$table->unsignedBigInteger('order_id')->nullable()->index();
|
||||
$table->unsignedBigInteger('fee_charged')->default(0);
|
||||
$table->unsignedBigInteger('triggered_price')->nullable();
|
||||
$table->timestamp('executed_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('conditional_orders');
|
||||
}
|
||||
};
|
||||
@@ -353,6 +353,18 @@ SELECT 'customer_direct_sms', 'false', 'پیامک تکی به مشتری از
|
||||
'switch'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'customer_direct_sms' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'enable_conditional_order', 'false', 'سفارش شرطی', 0, 1,
|
||||
'روشن: قابلیت سفارش شرطی سراسری فعال است. خاموش: منو و ارزیابی اجرا نمیشود. فقط از دیپلوی قابل تغییر است.',
|
||||
'switch'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'enable_conditional_order' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'fee_conditional_order', '0', 'کارمزد سفارش شرطی', 1, 1,
|
||||
'مبلغ ریالی که با هر بار ثبت تریگر از کیف پول ریالی مشتری کم میشود. صفر = رایگان.',
|
||||
'number'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_conditional_order' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`)
|
||||
SELECT 'sms_notif_order_admin', 'true', 'پیامک خرید/فروش به ادمین', 1, 1, NULL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'sms_notif_order_admin' LIMIT 1);
|
||||
|
||||
@@ -308,6 +308,18 @@ SELECT 'customer_direct_sms', 'false', 'پیامک تکی به مشتری از
|
||||
'switch'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'customer_direct_sms' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'enable_conditional_order', 'false', 'سفارش شرطی', 0, 1,
|
||||
'روشن: قابلیت سفارش شرطی سراسری فعال است. خاموش: منو و ارزیابی اجرا نمیشود. فقط از دیپلوی قابل تغییر است.',
|
||||
'switch'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'enable_conditional_order' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'fee_conditional_order', '0', 'کارمزد سفارش شرطی', 1, 1,
|
||||
'مبلغ ریالی که با هر بار ثبت تریگر از کیف پول ریالی مشتری کم میشود. صفر = رایگان.',
|
||||
'number'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_conditional_order' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`)
|
||||
SELECT 'sms_notif_order_admin', 'true', 'پیامک خرید/فروش به ادمین', 1, 1, NULL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'sms_notif_order_admin' LIMIT 1);
|
||||
|
||||
@@ -45,6 +45,12 @@ Route::middleware('auth:user')->group(function (){
|
||||
Route::put('price_triggers/{id}', [AdminPriceTriggerController::class, 'update']);
|
||||
Route::put('price_triggers/{id}/toggle', [AdminPriceTriggerController::class, 'toggleActive']);
|
||||
Route::delete('price_triggers/{id}', [AdminPriceTriggerController::class, 'destroy']);
|
||||
//------------ConditionalOrder
|
||||
Route::post('conditional_orders/table', [\App\Http\Controllers\Admin\ConditionalOrderController::class, 'table']);
|
||||
Route::post('conditional_orders', [\App\Http\Controllers\Admin\ConditionalOrderController::class, 'store']);
|
||||
Route::put('conditional_orders/{id}', [\App\Http\Controllers\Admin\ConditionalOrderController::class, 'update']);
|
||||
Route::put('conditional_orders/{id}/toggle', [\App\Http\Controllers\Admin\ConditionalOrderController::class, 'toggleActive']);
|
||||
Route::delete('conditional_orders/{id}', [\App\Http\Controllers\Admin\ConditionalOrderController::class, 'destroy']);
|
||||
//-----------product
|
||||
Route::post('/product/table', [ProductController::class, 'tableProduct']);
|
||||
Route::post('/product/archive/table', [ProductController::class, 'archiveTable']);
|
||||
@@ -89,6 +95,8 @@ Route::middleware('auth:user')->group(function (){
|
||||
Route::get('/customer/active/{id}',[CustomerController::class,'active']);
|
||||
Route::get('/customer/archive/{id}',[CustomerController::class,'archive']);
|
||||
Route::get('/customer/trade/{id}',[CustomerController::class,'changeTradeActive']);
|
||||
Route::get('/customer/conditional_order/{id}',[CustomerController::class,'changeConditionalOrderActive']);
|
||||
Route::put('/customer/conditional_order_limit/{id}',[CustomerController::class,'updateConditionalOrderLimit']);
|
||||
Route::post('/customer/archive/table',[CustomerController::class,'archiveTable']);
|
||||
Route::get('/customer/is_valid/{id}',[CustomerController::class,'MakeValidByAdmin']);
|
||||
Route::get('/customer/list',[CustomerController::class,'listUser']);
|
||||
|
||||
@@ -49,6 +49,12 @@ Route::middleware(['auth:customer', 'customer.active'])->group(function () use (
|
||||
|
||||
//---------PriceTrigger
|
||||
Route::resource('price_triggers', PriceTriggerController::class);
|
||||
//---------ConditionalOrder
|
||||
Route::get('conditional_orders', [\App\Http\Controllers\Customer\ConditionalOrderController::class, 'index']);
|
||||
Route::post('conditional_orders', [\App\Http\Controllers\Customer\ConditionalOrderController::class, 'store']);
|
||||
Route::put('conditional_orders/{id}', [\App\Http\Controllers\Customer\ConditionalOrderController::class, 'update']);
|
||||
Route::put('conditional_orders/{id}/toggle', [\App\Http\Controllers\Customer\ConditionalOrderController::class, 'toggleActive']);
|
||||
Route::delete('conditional_orders/{id}', [\App\Http\Controllers\Customer\ConditionalOrderController::class, 'destroy']);
|
||||
$cfg = app(ConfigService::class);
|
||||
if ($cfg->getBool('profit_gold_api_enabled', false)) {
|
||||
Route::get('profit_gold', [OrderController::class, 'profit_gold']);
|
||||
|
||||
Reference in New Issue
Block a user