feat(order): wait for colleague callback before settling wallet
Keep cover orders pending until colleague sts 2/4/6 via callback or 35s poll. Nightly prices:prune also drops old log tables. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\ColleagueOrderCallbackService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class PollPendingColleagueOrdersCommand extends Command
|
||||
{
|
||||
protected $signature = 'colleague:poll-pending-orders';
|
||||
|
||||
protected $description = 'Poll colleague for pending cover orders every 35s when callback is on';
|
||||
|
||||
public function handle(ColleagueOrderCallbackService $service): int
|
||||
{
|
||||
$first = $service->pollDueOrders();
|
||||
sleep(ColleagueOrderCallbackService::POLL_SECONDS);
|
||||
$second = $service->pollDueOrders();
|
||||
$this->info(json_encode(['first' => $first, 'second' => $second], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\DatabaseLogPruneService;
|
||||
use App\Services\PriceSnapshotService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
@@ -9,11 +10,14 @@ class PrunePriceSnapshotsCommand extends Command
|
||||
{
|
||||
protected $signature = 'prices:prune';
|
||||
|
||||
protected $description = 'Downsample and prune price history (daily at midnight)';
|
||||
protected $description = 'Nightly prune: price history + bloating logs (31-day retention)';
|
||||
|
||||
public function handle(PriceSnapshotService $snapshots): int
|
||||
public function handle(PriceSnapshotService $snapshots, DatabaseLogPruneService $logs): int
|
||||
{
|
||||
$stats = $snapshots->pruneHistory();
|
||||
$stats = [
|
||||
'prices' => $snapshots->pruneHistory(),
|
||||
'logs' => $logs->pruneHistory(),
|
||||
];
|
||||
$this->info(json_encode($stats, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
return self::SUCCESS;
|
||||
|
||||
@@ -31,10 +31,11 @@ class Kernel extends ConsoleKernel
|
||||
->name('refresh_zaarbox_prices')
|
||||
->withoutOverlapping();
|
||||
|
||||
// پاکسازی و downsample تاریخچه — ساعت ۱۲ شب
|
||||
// پاکسازی و downsample تاریخچه قیمت + لاگهای حجیم (۳۱ روز) — ساعت ۱۲ شب
|
||||
$schedule->command('prices:prune')
|
||||
->dailyAt('00:00')
|
||||
->name('prune_price_snapshots');
|
||||
->name('prune_price_snapshots')
|
||||
->withoutOverlapping();
|
||||
|
||||
// ترمیم اسناد ریالی جاافتادهٔ پرگار — سریع برای موارد تازه
|
||||
$schedule->command('pargar:requeue-wallet-documents --execute --days=14')
|
||||
@@ -52,6 +53,11 @@ class Kernel extends ConsoleKernel
|
||||
->everyMinute()
|
||||
->name('automations_run')
|
||||
->withoutOverlapping(50);
|
||||
|
||||
$schedule->command('colleague:poll-pending-orders')
|
||||
->everyMinute()
|
||||
->name('colleague_poll_pending_orders')
|
||||
->withoutOverlapping(58);
|
||||
}
|
||||
|
||||
protected function commands()
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Customer;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\ColleagueOrderCallbackService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ColleagueOrderCallbackController extends Controller
|
||||
{
|
||||
public function callback(Request $request, ?int $order = null)
|
||||
{
|
||||
$body = $request->all();
|
||||
if ($body === []) {
|
||||
$decoded = json_decode((string) $request->getContent(), true);
|
||||
$body = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
$result = app(ColleagueOrderCallbackService::class)->handleCallbackPayload($body, $order);
|
||||
|
||||
return response()->json($result, !empty($result['ok']) ? 200 : 422);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ use App\Services\FormulaPriceService;
|
||||
use App\Services\ReferralGiftService;
|
||||
use App\Services\WalletBalanceService;
|
||||
use App\Services\WalletBroadcastService;
|
||||
use App\Services\ColleagueOrderCallbackService;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
|
||||
class SingleOrderController extends Controller
|
||||
@@ -115,7 +117,7 @@ class SingleOrderController extends Controller
|
||||
throw new UnprocessableEntityHttpException('کیف پول یافت نشد.');
|
||||
}
|
||||
$response = [];
|
||||
|
||||
$this->assertNoOpenColleagueOrder((int) ($data['customer_id'] ?? 0));
|
||||
|
||||
// Check if the user wants to transact with a product or non-product (gold)
|
||||
if (isset($data['product_id']) and $data['product'] = Product::find($data['product_id'])) {
|
||||
@@ -209,101 +211,12 @@ class SingleOrderController extends Controller
|
||||
$response['sell'] = $this->processSellTransaction($data, $wallet, $wallet_interface);
|
||||
}
|
||||
}
|
||||
// دفترکل داخلی همیشه ثبت شود — مستقل از اتصال به حسابداری بیرونی
|
||||
if (isset($data['order'])) {
|
||||
try {
|
||||
app(\App\Services\AccountingLedgerService::class)->recordTrade($data['order']);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
|
||||
if (!empty($data['_awaiting_callback']) && isset($data['order']) && $data['order'] instanceof Order) {
|
||||
return $this->pendingColleagueStoreResponse($data['order']);
|
||||
}
|
||||
|
||||
if ($config->isTahesabAccounting()) {
|
||||
try {
|
||||
$data['tahesab'] = app(TahesabController::class)->storeTrade($data);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isZarinAccounting()) {
|
||||
try {
|
||||
$data['zarin'] = app(\App\Http\Controllers\Admin\ZarinController::class)->storeTrade($data);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isPargarAccounting()) {
|
||||
try {
|
||||
$paymentProductId = (int) ($data['payment_product_id'] ?? 0);
|
||||
$paymentKind = app(\App\Services\PargarSyncService::class)
|
||||
->inferPaymentKindFromContext([
|
||||
'payment_product_id' => $paymentProductId,
|
||||
]);
|
||||
// خرید/فروش عادی از کیف ریالی (بدون payment_product_id) = نقدی
|
||||
if ($paymentProductId <= 0) {
|
||||
$paymentKind = \App\Services\PargarSyncService::PAYMENT_CASH;
|
||||
}
|
||||
$paymentGold = null;
|
||||
if ($paymentKind === \App\Services\PargarSyncService::PAYMENT_MAIN_GOLD) {
|
||||
$paymentGold = (float) ($data['total_amount'] ?? $data['amount'] ?? 0);
|
||||
}
|
||||
app(\App\Services\PargarSyncService::class)->recordTrade(
|
||||
$data['order'] ?? null,
|
||||
false,
|
||||
[
|
||||
'payment_kind' => $paymentKind,
|
||||
'payment_product_id' => $paymentProductId > 0 ? $paymentProductId : null,
|
||||
'product_id' => (int) ($data['product_id'] ?? 0) ?: null,
|
||||
'number' => isset($data['number']) ? (float) $data['number'] : null,
|
||||
'payment_gold' => $paymentGold,
|
||||
]
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['order'])) {
|
||||
try {
|
||||
app(\App\Services\ColleagueAccountingService::class)->recordCoveredTrade($data);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
$response['reward'] = $this->give_order_reward($data, $wallet, $wallet_interface);
|
||||
if ((int) $data['direction'] === 1 && $customer) {
|
||||
$referrer = Customer::where('referral_id', $customer->referral_to_id)->first();
|
||||
if (!empty($referrer) && $referrer->first_buy_order == 1 && $customer->first_buy_order !== 1) {
|
||||
$customer->first_buy_order = true;
|
||||
$customer->save();
|
||||
$response['bonus_gift'] = $this->bonusGift($customer, $referrer);
|
||||
}
|
||||
}
|
||||
if ($customer && isset($data['order'])) {
|
||||
try {
|
||||
app(ReferralGiftService::class)->tryGiveOnFirstOrder($customer);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['order'])) {
|
||||
try {
|
||||
$response['Notifications'] = $this->sendTransactionNotifications($data, $transactionType);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($customer) {
|
||||
app(WalletBroadcastService::class)->broadcastForCustomer(
|
||||
(int) $customer->id,
|
||||
$ownerType
|
||||
);
|
||||
}
|
||||
$this->runSuccessfulTradeAftermath($data, $customer, $wallet, $wallet_interface, $transactionType, $ownerType, $response);
|
||||
|
||||
return response()->json([$response, $data]);
|
||||
}
|
||||
@@ -362,6 +275,24 @@ class SingleOrderController extends Controller
|
||||
$productId = (int) ($data['product_id'] ?? $abshode['id'] ?? 0);
|
||||
$usesProductWallet = $productId > 0 && !$config->isMainWalletProduct($productId);
|
||||
|
||||
if (!empty($data['_awaiting_callback'])) {
|
||||
$this->persistColleagueFinalizePayload($order, [
|
||||
'kind' => 'buy',
|
||||
'product_id' => $productId,
|
||||
'uses_product_wallet' => $usesProductWallet,
|
||||
'payment_amount' => $data['payment_amount'] ?? null,
|
||||
'amount' => $data['amount'] ?? null,
|
||||
'total_price' => $data['total_price'] ?? null,
|
||||
'number' => $data['number'] ?? null,
|
||||
'payment_product_id' => $data['payment_product_id'] ?? null,
|
||||
'abshode_id' => $data['abshode_id'] ?? null,
|
||||
'direction' => $data['direction'] ?? 1,
|
||||
'customer_id' => $data['customer_id'] ?? null,
|
||||
]);
|
||||
|
||||
return ['pending' => true];
|
||||
}
|
||||
|
||||
$this->runAfterProviderCover($order, $data, function () use (&$data, $wallet_interface, $productId, $usesProductWallet, $order) {
|
||||
if ($usesProductWallet) {
|
||||
new TransactionInterface($wallet_interface, [
|
||||
@@ -642,6 +573,25 @@ class SingleOrderController extends Controller
|
||||
|
||||
$order = $this->openOrderThenCoverWithProvider($data);
|
||||
|
||||
if (!empty($data['_awaiting_callback'])) {
|
||||
$this->persistColleagueFinalizePayload($order, [
|
||||
'kind' => 'sell',
|
||||
'product_id' => $productId,
|
||||
'uses_product_wallet' => $usesProductWallet,
|
||||
'gold_debit' => $goldDebit,
|
||||
'payment_amount' => $data['payment_amount'] ?? null,
|
||||
'amount' => $data['amount'] ?? null,
|
||||
'total_price' => $data['total_price'] ?? null,
|
||||
'number' => $data['number'] ?? null,
|
||||
'payment_product_id' => $data['payment_product_id'] ?? null,
|
||||
'abshode_id' => $data['abshode_id'] ?? null,
|
||||
'direction' => $data['direction'] ?? 2,
|
||||
'customer_id' => $data['customer_id'] ?? null,
|
||||
]);
|
||||
|
||||
return ['pending' => true];
|
||||
}
|
||||
|
||||
$this->runAfterProviderCover($order, $data, function () use (&$data, $wallet_interface, $wallet_interface_product, $productId, $usesProductWallet, $goldDebit, $order) {
|
||||
if ($usesProductWallet) {
|
||||
$this->normalizeProductWalletGold($wallet_interface_product);
|
||||
@@ -1060,6 +1010,9 @@ class SingleOrderController extends Controller
|
||||
$this->assertProviderOrderAccepted($data);
|
||||
$this->finalizeProviderOrder($data);
|
||||
$order->refresh();
|
||||
if (app(ColleagueOrderCallbackService::class)->waitEnabled()) {
|
||||
$data['_awaiting_callback'] = true;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->markOrderFailed($order, $e->getMessage());
|
||||
throw $e instanceof UnprocessableEntityHttpException
|
||||
@@ -2066,4 +2019,322 @@ class SingleOrderController extends Controller
|
||||
|
||||
return (float) ($data['payment_amount'] ?? $data['total_price'] ?? 0);
|
||||
}
|
||||
|
||||
private function assertNoOpenColleagueOrder(int $customerId): void
|
||||
{
|
||||
if ($customerId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$open = Order::query()->where('customer_id', $customerId)->where('sts', 4)->exists();
|
||||
if ($open) {
|
||||
throw new UnprocessableEntityHttpException(
|
||||
'سفارش قبلی شما هنوز در انتظار تأیید همکار است.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function pendingColleagueStoreResponse(Order $order)
|
||||
{
|
||||
$sts = (int) $order->sts;
|
||||
|
||||
return response()->json([
|
||||
[
|
||||
'pending' => true,
|
||||
'sts' => $sts,
|
||||
'sts_str' => Order::$STS[$sts] ?? 'در انتظار همکار',
|
||||
'order_id' => $order->id,
|
||||
],
|
||||
[
|
||||
'order' => $order,
|
||||
'pending' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function pendingColleague()
|
||||
{
|
||||
$order = app(ColleagueOrderCallbackService::class)
|
||||
->pendingForCustomer((int) auth('customer')->id());
|
||||
|
||||
return response()->json(app(ColleagueOrderCallbackService::class)->publicStatusPayload($order));
|
||||
}
|
||||
|
||||
public function colleagueStatus(int $id)
|
||||
{
|
||||
$order = Order::query()
|
||||
->where('id', $id)
|
||||
->where('customer_id', auth('customer')->id())
|
||||
->first();
|
||||
|
||||
return response()->json(app(ColleagueOrderCallbackService::class)->publicStatusPayload($order));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function persistColleagueFinalizePayload(Order $order, array $payload): void
|
||||
{
|
||||
if (!Schema::hasColumn('orders', 'colleague_callback_payload')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order->colleague_callback_payload = $payload;
|
||||
$order->save();
|
||||
}
|
||||
|
||||
public function rejectColleaguePendingOrder(Order $order, string $message): void
|
||||
{
|
||||
$this->markOrderFailed($order, $message);
|
||||
}
|
||||
|
||||
public function finalizeColleaguePendingOrder(Order $order): void
|
||||
{
|
||||
$payload = $order->colleague_callback_payload;
|
||||
if (!is_array($payload)) {
|
||||
$payload = [];
|
||||
}
|
||||
|
||||
$customer = Customer::findOrFail((int) $order->customer_id);
|
||||
$ownerType = (int) ($customer->type ?? 2);
|
||||
[$wallet, $wallet_interface] = $this->resolveOrderWallet((int) $customer->id, $customer, $ownerType);
|
||||
if (!$wallet) {
|
||||
throw new UnprocessableEntityHttpException('کیف پول یافت نشد.');
|
||||
}
|
||||
|
||||
$data = array_merge($order->toArray(), $payload);
|
||||
$data['order'] = $order;
|
||||
$data['customer_id'] = (int) $order->customer_id;
|
||||
$productId = (int) ($payload['product_id'] ?? $order->product_id ?? 0);
|
||||
if ($productId > 0) {
|
||||
$data['product'] = Product::find($productId);
|
||||
$data['product_id'] = $productId;
|
||||
}
|
||||
|
||||
$kind = (string) ($payload['kind'] ?? (((int) $order->direction === 2) ? 'sell' : 'buy'));
|
||||
$usesProductWallet = (bool) ($payload['uses_product_wallet'] ?? false);
|
||||
$goldDebit = (float) ($payload['gold_debit'] ?? $order->amount ?? 0);
|
||||
|
||||
$this->runAfterProviderCover($order, $data, function () use (
|
||||
&$data,
|
||||
$wallet_interface,
|
||||
$productId,
|
||||
$usesProductWallet,
|
||||
$goldDebit,
|
||||
$order,
|
||||
$kind
|
||||
) {
|
||||
if ($kind === 'sell') {
|
||||
$this->applySellWalletAfterColleague($data, $wallet_interface, $productId, $usesProductWallet, $goldDebit, $order);
|
||||
} else {
|
||||
$this->applyBuyWalletAfterColleague($data, $wallet_interface, $productId, $usesProductWallet, $order);
|
||||
}
|
||||
});
|
||||
|
||||
$transactionType = ((int) $order->direction === 2) ? 'فروش' : 'خرید';
|
||||
$response = [];
|
||||
$this->runSuccessfulTradeAftermath($data, $customer, $wallet, $wallet_interface, $transactionType, $ownerType, $response);
|
||||
}
|
||||
|
||||
private function applyBuyWalletAfterColleague(
|
||||
array &$data,
|
||||
$wallet_interface,
|
||||
int $productId,
|
||||
bool $usesProductWallet,
|
||||
Order $order
|
||||
): void {
|
||||
if ($usesProductWallet) {
|
||||
new TransactionInterface($wallet_interface, [
|
||||
'cash' => $data['payment_amount'],
|
||||
'product_id' => $productId,
|
||||
'expire_at' => now(),
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'start_at' => now(),
|
||||
'title' => 'برداشت وجه نقد',
|
||||
], 12, false, false);
|
||||
$wallet_interface_product = $this->productWalletInterface($productId);
|
||||
$this->normalizeProductWalletGold($wallet_interface_product);
|
||||
new TransactionInterface($wallet_interface_product, [
|
||||
'gold' => $data['amount'],
|
||||
'product_id' => $productId,
|
||||
'expire_at' => now(),
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'start_at' => now(),
|
||||
'title' => $this->meltedOrProductBuyTitle($data),
|
||||
], 1, true, false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$buyTransaction = [
|
||||
'cash' => $data['payment_amount'],
|
||||
'gold' => $data['amount'],
|
||||
'expire_at' => now(),
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'start_at' => now(),
|
||||
'title' => $this->meltedOrProductBuyTitle($data),
|
||||
];
|
||||
if ($productId > 0) {
|
||||
$buyTransaction['product_id'] = $productId;
|
||||
$buyTransaction['note'] = 'product_id:' . $productId;
|
||||
}
|
||||
new TransactionInterface($wallet_interface, $buyTransaction, 1, true, false);
|
||||
}
|
||||
|
||||
private function applySellWalletAfterColleague(
|
||||
array &$data,
|
||||
$wallet_interface,
|
||||
int $productId,
|
||||
bool $usesProductWallet,
|
||||
float $goldDebit,
|
||||
Order $order
|
||||
): void {
|
||||
if ($usesProductWallet) {
|
||||
$wallet_interface_product = $this->productWalletInterface($productId);
|
||||
$this->normalizeProductWalletGold($wallet_interface_product);
|
||||
new TransactionInterface($wallet_interface_product, [
|
||||
'gold' => $goldDebit,
|
||||
'product_id' => $productId,
|
||||
'expire_at' => now(),
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'start_at' => now(),
|
||||
'title' => $this->meltedOrProductSellTitle($data),
|
||||
], 1, false, false);
|
||||
new TransactionInterface($wallet_interface, [
|
||||
'cash' => $data['total_price'],
|
||||
'product_id' => $productId,
|
||||
'expire_at' => now(),
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'start_at' => now(),
|
||||
'title' => 'واریز وجه نقد',
|
||||
], 12, false, true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$sellTransaction = [
|
||||
'gold' => $goldDebit,
|
||||
'cash' => $data['total_price'],
|
||||
'expire_at' => now(),
|
||||
'active' => true,
|
||||
'order_id' => $order->id,
|
||||
'title' => $this->meltedOrProductSellTitle($data),
|
||||
];
|
||||
if ($productId > 0) {
|
||||
$sellTransaction['product_id'] = $productId;
|
||||
$sellTransaction['note'] = 'product_id:' . $productId;
|
||||
}
|
||||
new TransactionInterface($wallet_interface, $sellTransaction, 2, false, true);
|
||||
}
|
||||
|
||||
private function runSuccessfulTradeAftermath(
|
||||
array &$data,
|
||||
$customer,
|
||||
$wallet,
|
||||
$wallet_interface,
|
||||
string $transactionType,
|
||||
int $ownerType,
|
||||
array &$response
|
||||
): void {
|
||||
$config = app(ConfigService::class);
|
||||
|
||||
if (isset($data['order'])) {
|
||||
try {
|
||||
app(\App\Services\AccountingLedgerService::class)->recordTrade($data['order']);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isTahesabAccounting()) {
|
||||
try {
|
||||
$data['tahesab'] = app(TahesabController::class)->storeTrade($data);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isZarinAccounting()) {
|
||||
try {
|
||||
$data['zarin'] = app(\App\Http\Controllers\Admin\ZarinController::class)->storeTrade($data);
|
||||
} catch (\Exception $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->isPargarAccounting()) {
|
||||
try {
|
||||
$paymentProductId = (int) ($data['payment_product_id'] ?? 0);
|
||||
$paymentKind = app(\App\Services\PargarSyncService::class)
|
||||
->inferPaymentKindFromContext([
|
||||
'payment_product_id' => $paymentProductId,
|
||||
]);
|
||||
if ($paymentProductId <= 0) {
|
||||
$paymentKind = \App\Services\PargarSyncService::PAYMENT_CASH;
|
||||
}
|
||||
$paymentGold = null;
|
||||
if ($paymentKind === \App\Services\PargarSyncService::PAYMENT_MAIN_GOLD) {
|
||||
$paymentGold = (float) ($data['total_amount'] ?? $data['amount'] ?? 0);
|
||||
}
|
||||
app(\App\Services\PargarSyncService::class)->recordTrade(
|
||||
$data['order'] ?? null,
|
||||
false,
|
||||
[
|
||||
'payment_kind' => $paymentKind,
|
||||
'payment_product_id' => $paymentProductId > 0 ? $paymentProductId : null,
|
||||
'product_id' => (int) ($data['product_id'] ?? 0) ?: null,
|
||||
'number' => isset($data['number']) ? (float) $data['number'] : null,
|
||||
'payment_gold' => $paymentGold,
|
||||
]
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['order'])) {
|
||||
try {
|
||||
app(\App\Services\ColleagueAccountingService::class)->recordCoveredTrade($data);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
$response['reward'] = $this->give_order_reward($data, $wallet, $wallet_interface);
|
||||
if ((int) ($data['direction'] ?? 0) === 1 && $customer) {
|
||||
$referrer = Customer::where('referral_id', $customer->referral_to_id)->first();
|
||||
if (!empty($referrer) && $referrer->first_buy_order == 1 && $customer->first_buy_order !== 1) {
|
||||
$customer->first_buy_order = true;
|
||||
$customer->save();
|
||||
$response['bonus_gift'] = $this->bonusGift($customer, $referrer);
|
||||
}
|
||||
}
|
||||
if ($customer && isset($data['order'])) {
|
||||
try {
|
||||
app(ReferralGiftService::class)->tryGiveOnFirstOrder($customer);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($data['order'])) {
|
||||
try {
|
||||
$response['Notifications'] = $this->sendTransactionNotifications($data, $transactionType);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
if ($customer) {
|
||||
app(WalletBroadcastService::class)->broadcastForCustomer(
|
||||
(int) $customer->id,
|
||||
$ownerType
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,14 @@ class Order extends Model
|
||||
'provider_id',
|
||||
'total_price_montzar',
|
||||
'description',
|
||||
'colleague_callback_payload',
|
||||
'colleague_last_poll_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'fee_breakdown' => 'array',
|
||||
'colleague_callback_payload' => 'array',
|
||||
'colleague_last_poll_at' => 'datetime',
|
||||
];
|
||||
|
||||
static $STS = [
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\Single\SingleOrderController;
|
||||
use App\Models\Order;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class ColleagueOrderCallbackService
|
||||
{
|
||||
public const POLL_SECONDS = 35;
|
||||
|
||||
public const SSE_EVENT = 'colleague_order';
|
||||
|
||||
public function waitEnabled(): bool
|
||||
{
|
||||
$config = app(ConfigService::class);
|
||||
|
||||
return $config->usesMontzarPricing() && $config->getBool('montzar_order_wait_callback', true);
|
||||
}
|
||||
|
||||
public function callbackUrl(int $localOrderId): string
|
||||
{
|
||||
$origin = rtrim(app(ConfigService::class)->resolvePublicSiteUrl(), '/');
|
||||
if ($origin === '') {
|
||||
$origin = rtrim((string) config('app.url'), '/');
|
||||
$origin = (string) preg_replace('#/server/?$#', '', $origin);
|
||||
}
|
||||
|
||||
return $origin . '/server/customer/colleague/order/callback/' . $localOrderId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $body
|
||||
*/
|
||||
public function handleCallbackPayload(array $body, ?int $localOrderId = null): array
|
||||
{
|
||||
$order = $this->findPendingOrder($body, $localOrderId);
|
||||
if (!$order) {
|
||||
return ['ok' => false, 'message' => 'سفارش در انتظار یافت نشد'];
|
||||
}
|
||||
|
||||
$sts = $this->providerStsFromBody($body);
|
||||
if ($sts === null) {
|
||||
return ['ok' => false, 'message' => 'وضعیت نامعتبر'];
|
||||
}
|
||||
|
||||
$this->rememberProviderId($order, $body);
|
||||
|
||||
return $this->applyProviderSts($order, $sts, $body);
|
||||
}
|
||||
|
||||
public function pollDueOrders(): int
|
||||
{
|
||||
if (!Schema::hasTable('orders') || !Schema::hasColumn('orders', 'sts')) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$cutoff = now()->subSeconds(self::POLL_SECONDS);
|
||||
$handled = 0;
|
||||
|
||||
$query = Order::query()->where('sts', 4)->whereNotNull('provider_id');
|
||||
if (Schema::hasColumn('orders', 'colleague_last_poll_at')) {
|
||||
$query->where(function ($q) use ($cutoff) {
|
||||
$q->where(function ($inner) use ($cutoff) {
|
||||
$inner->whereNull('colleague_last_poll_at')
|
||||
->where('created_at', '<=', $cutoff);
|
||||
})->orWhere('colleague_last_poll_at', '<=', $cutoff);
|
||||
});
|
||||
} else {
|
||||
$query->where('created_at', '<=', $cutoff);
|
||||
}
|
||||
|
||||
$orders = $query->orderBy('id')->limit(80)->get();
|
||||
foreach ($orders as $order) {
|
||||
try {
|
||||
$this->pollOne($order);
|
||||
$handled++;
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Colleague pending poll failed', [
|
||||
'order_id' => $order->id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
$this->touchPollAt($order);
|
||||
}
|
||||
}
|
||||
|
||||
return $handled;
|
||||
}
|
||||
|
||||
public function pollOne(Order $order): array
|
||||
{
|
||||
$providerId = (int) ($order->provider_id ?? 0);
|
||||
if ($providerId <= 0) {
|
||||
$this->touchPollAt($order);
|
||||
|
||||
return ['ok' => true, 'state' => 'wait'];
|
||||
}
|
||||
|
||||
$body = app(MontzarService::class)->fetchColleagueOrder($providerId);
|
||||
$this->touchPollAt($order);
|
||||
if (!is_array($body)) {
|
||||
return ['ok' => true, 'state' => 'wait'];
|
||||
}
|
||||
|
||||
$sts = $this->providerStsFromBody($body);
|
||||
if ($sts === null) {
|
||||
return ['ok' => true, 'state' => 'wait'];
|
||||
}
|
||||
|
||||
return $this->applyProviderSts($order, $sts, $body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $body
|
||||
*/
|
||||
public function applyProviderSts(Order $order, int $providerSts, array $body = []): array
|
||||
{
|
||||
if (in_array($providerSts, [4, 6], true)) {
|
||||
return $this->rejectPending($order, $body);
|
||||
}
|
||||
|
||||
if ($providerSts === 2) {
|
||||
return $this->approvePending($order, $body);
|
||||
}
|
||||
|
||||
return ['ok' => true, 'state' => 'wait', 'sts' => (int) $order->sts];
|
||||
}
|
||||
|
||||
public function pendingForCustomer(int $customerId): ?Order
|
||||
{
|
||||
if ($customerId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Order::query()
|
||||
->where('customer_id', $customerId)
|
||||
->where('sts', 4)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
public function publicStatusPayload(?Order $order): array
|
||||
{
|
||||
if (!$order) {
|
||||
return [
|
||||
'pending' => false,
|
||||
'order' => null,
|
||||
'sts' => null,
|
||||
'sts_str' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$sts = (int) $order->sts;
|
||||
|
||||
return [
|
||||
'pending' => $sts === 4,
|
||||
'order_id' => $order->id,
|
||||
'sts' => $sts,
|
||||
'sts_str' => Order::$STS[$sts] ?? null,
|
||||
'order' => $order,
|
||||
];
|
||||
}
|
||||
|
||||
public function broadcast(Order $order): void
|
||||
{
|
||||
$customerId = (int) ($order->customer_id ?? 0);
|
||||
if ($customerId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
app(PriceStreamService::class)->publishForCustomer($customerId, self::SSE_EVENT, [
|
||||
'order_id' => $order->id,
|
||||
'sts' => (int) $order->sts,
|
||||
'sts_str' => Order::$STS[(int) $order->sts] ?? null,
|
||||
'pending' => (int) $order->sts === 4,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $body
|
||||
*/
|
||||
private function findPendingOrder(array $body, ?int $localOrderId): ?Order
|
||||
{
|
||||
if ($localOrderId && $localOrderId > 0) {
|
||||
$order = Order::query()->where('id', $localOrderId)->where('sts', 4)->first();
|
||||
if ($order) {
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
|
||||
$providerId = $this->providerIdFromBody($body);
|
||||
if ($providerId > 0) {
|
||||
return Order::query()
|
||||
->where('provider_id', $providerId)
|
||||
->where('sts', 4)
|
||||
->orderByDesc('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $body
|
||||
*/
|
||||
private function rememberProviderId(Order $order, array $body): void
|
||||
{
|
||||
$providerId = $this->providerIdFromBody($body);
|
||||
if ($providerId <= 0 || !Schema::hasColumn('orders', 'provider_id')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((int) ($order->provider_id ?? 0) === $providerId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order->provider_id = $providerId;
|
||||
$order->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $body
|
||||
*/
|
||||
private function approvePending(Order $order, array $body): array
|
||||
{
|
||||
$this->rememberProviderId($order, $body);
|
||||
|
||||
$done = false;
|
||||
DB::transaction(function () use ($order, &$done) {
|
||||
$locked = Order::query()->where('id', $order->id)->lockForUpdate()->first();
|
||||
if (!$locked || (int) $locked->sts !== 4) {
|
||||
return;
|
||||
}
|
||||
app(SingleOrderController::class)->finalizeColleaguePendingOrder($locked);
|
||||
$done = true;
|
||||
});
|
||||
|
||||
$fresh = Order::find($order->id);
|
||||
if ($fresh) {
|
||||
$this->broadcast($fresh);
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'state' => $done ? 'approved' : 'ignored',
|
||||
'sts' => $fresh ? (int) $fresh->sts : 2,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $body
|
||||
*/
|
||||
private function rejectPending(Order $order, array $body): array
|
||||
{
|
||||
DB::transaction(function () use ($order, $body) {
|
||||
$locked = Order::query()->where('id', $order->id)->lockForUpdate()->first();
|
||||
if (!$locked || (int) $locked->sts !== 4) {
|
||||
return;
|
||||
}
|
||||
$message = trim((string) ($body['sts_str'] ?? $body['message'] ?? 'رد شده توسط همکار'));
|
||||
app(SingleOrderController::class)->rejectColleaguePendingOrder($locked, $message !== '' ? $message : 'رد شده توسط همکار');
|
||||
});
|
||||
|
||||
$fresh = Order::find($order->id);
|
||||
if ($fresh) {
|
||||
$this->broadcast($fresh);
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'state' => 'rejected',
|
||||
'sts' => $fresh ? (int) $fresh->sts : 3,
|
||||
];
|
||||
}
|
||||
|
||||
private function touchPollAt(Order $order): void
|
||||
{
|
||||
if (!Schema::hasColumn('orders', 'colleague_last_poll_at')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order->colleague_last_poll_at = now();
|
||||
$order->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $body
|
||||
*/
|
||||
public function providerStsFromBody(array $body): ?int
|
||||
{
|
||||
$rows = [$body, $body['data'] ?? null, $body['order'] ?? null];
|
||||
foreach ($body as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$rows[] = $item;
|
||||
$rows[] = $item['order'] ?? null;
|
||||
$rows[] = $item['data'] ?? null;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row) || !array_key_exists('sts', $row) || $row['sts'] === null || $row['sts'] === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
return (int) $row['sts'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $body
|
||||
*/
|
||||
private function providerIdFromBody(array $body): int
|
||||
{
|
||||
foreach ([$body, $body['data'] ?? null, $body['order'] ?? null] as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
foreach (['order_id', 'id', 'provider_order_id'] as $key) {
|
||||
$value = (int) ($row[$key] ?? 0);
|
||||
if ($value > 0) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Log;
|
||||
use App\Models\PargarSyncLog;
|
||||
use App\Models\PriceSyncLog;
|
||||
use App\Models\PriceSyncLogItem;
|
||||
use App\Models\Tahesab;
|
||||
use App\Models\Zarin;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* پاکسازی شبانهٔ لاگهای حجیم — همان سقف نگهداری جدول prices (۳۱ روز).
|
||||
*/
|
||||
class DatabaseLogPruneService
|
||||
{
|
||||
/** همراستا با حذف قطعی در PriceSnapshotService::pruneHistory */
|
||||
public const RETENTION_DAYS = 31;
|
||||
|
||||
private const CHUNK = 500;
|
||||
|
||||
public function pruneHistory(?Carbon $now = null): array
|
||||
{
|
||||
$cutoff = ($now ?? now())->copy()->subDays(self::RETENTION_DAYS);
|
||||
|
||||
return [
|
||||
'retention_days' => self::RETENTION_DAYS,
|
||||
'cutoff' => $cutoff->toDateTimeString(),
|
||||
'price_sync_logs' => $this->prunePriceSyncLogs($cutoff),
|
||||
'logs' => $this->pruneGenericLogs($cutoff),
|
||||
'pargar_sync_logs' => $this->pruneSuccessfulRows(
|
||||
PargarSyncLog::query(),
|
||||
'pargar_sync_logs',
|
||||
PargarSyncService::STATUS_SUCCESS,
|
||||
$cutoff
|
||||
),
|
||||
'zarin' => $this->pruneSuccessfulRows(
|
||||
Zarin::query(),
|
||||
'zarin',
|
||||
ZarinSyncService::STATUS_SUCCESS,
|
||||
$cutoff
|
||||
),
|
||||
'tahesab' => $this->pruneSuccessfulRows(
|
||||
Tahesab::query(),
|
||||
'tahesab',
|
||||
TahesabSyncService::STATUS_SUCCESS,
|
||||
$cutoff
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{deleted: int, items_deleted: int} */
|
||||
private function prunePriceSyncLogs(Carbon $cutoff): array
|
||||
{
|
||||
if (! Schema::hasTable('price_sync_logs')) {
|
||||
return ['deleted' => 0, 'items_deleted' => 0];
|
||||
}
|
||||
|
||||
$deleted = 0;
|
||||
$itemsDeleted = 0;
|
||||
$hasItems = Schema::hasTable('price_sync_log_items');
|
||||
|
||||
PriceSyncLog::query()
|
||||
->where('created_at', '<', $cutoff)
|
||||
->orderBy('id')
|
||||
->select(['id'])
|
||||
->chunkById(self::CHUNK, function ($rows) use (&$deleted, &$itemsDeleted, $hasItems) {
|
||||
$ids = $rows->pluck('id')->all();
|
||||
if ($ids === []) {
|
||||
return;
|
||||
}
|
||||
if ($hasItems) {
|
||||
$itemsDeleted += (int) PriceSyncLogItem::query()->whereIn('log_id', $ids)->delete();
|
||||
}
|
||||
$deleted += (int) PriceSyncLog::query()->whereIn('id', $ids)->delete();
|
||||
});
|
||||
|
||||
return ['deleted' => $deleted, 'items_deleted' => $itemsDeleted];
|
||||
}
|
||||
|
||||
/** @return array{deleted: int} */
|
||||
private function pruneGenericLogs(Carbon $cutoff): array
|
||||
{
|
||||
if (! Schema::hasTable('logs')) {
|
||||
return ['deleted' => 0];
|
||||
}
|
||||
|
||||
$deleted = 0;
|
||||
|
||||
Log::query()
|
||||
->where('created_at', '<', $cutoff)
|
||||
->orderBy('id')
|
||||
->select(['id'])
|
||||
->chunkById(self::CHUNK, function ($rows) use (&$deleted) {
|
||||
$ids = $rows->pluck('id')->all();
|
||||
if ($ids === []) {
|
||||
return;
|
||||
}
|
||||
$deleted += (int) Log::query()->whereIn('id', $ids)->delete();
|
||||
});
|
||||
|
||||
return ['deleted' => $deleted];
|
||||
}
|
||||
|
||||
/**
|
||||
* فقط ردیفهای موفق حسابداری — pending/failed برای retry میمانند.
|
||||
*
|
||||
* @return array{deleted: int, skipped: bool}
|
||||
*/
|
||||
private function pruneSuccessfulRows(Builder $query, string $table, string $successStatus, Carbon $cutoff): array
|
||||
{
|
||||
if (! Schema::hasTable($table) || ! Schema::hasColumn($table, 'sync_status')) {
|
||||
return ['deleted' => 0, 'skipped' => true];
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn($table, 'created_at')) {
|
||||
return ['deleted' => 0, 'skipped' => true];
|
||||
}
|
||||
|
||||
$deleted = 0;
|
||||
|
||||
(clone $query)
|
||||
->where('sync_status', $successStatus)
|
||||
->where('created_at', '<', $cutoff)
|
||||
->orderBy('id')
|
||||
->select(['id'])
|
||||
->chunkById(self::CHUNK, function ($rows) use (&$deleted, $query) {
|
||||
$ids = $rows->pluck('id')->all();
|
||||
if ($ids === []) {
|
||||
return;
|
||||
}
|
||||
$deleted += (int) (clone $query)->whereIn('id', $ids)->delete();
|
||||
});
|
||||
|
||||
return ['deleted' => $deleted, 'skipped' => false];
|
||||
}
|
||||
}
|
||||
@@ -616,7 +616,7 @@ class MontzarService
|
||||
? auth('customer')->user()->name
|
||||
: ($data['customer']['name'] ?? '');
|
||||
|
||||
return [
|
||||
$payload = [
|
||||
'mazaneh' => (int) $mazaneh,
|
||||
'amount' => (float) $data['amount'],
|
||||
'description' => $this->config()->Config('name_fa') . ' - ' . $customerName,
|
||||
@@ -625,6 +625,56 @@ class MontzarService
|
||||
'cash' => $cash,
|
||||
'input_mode' => 'gold',
|
||||
];
|
||||
|
||||
if (
|
||||
$this->config()->getBool('montzar_order_wait_callback', true)
|
||||
&& isset($data['order'])
|
||||
&& $data['order'] instanceof Order
|
||||
) {
|
||||
$payload['callback'] = app(ColleagueOrderCallbackService::class)->callbackUrl((int) $data['order']->id);
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET وضعیت سفارش روی نود همکار — مسیر از setting + شناسه سفارش همکار.
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function fetchColleagueOrder(int $providerOrderId): ?array
|
||||
{
|
||||
if ($providerOrderId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = trim($this->config()->getString('montzar_order_status_path', '/server/customer/order'));
|
||||
if ($path === '') {
|
||||
$path = '/server/customer/order';
|
||||
}
|
||||
|
||||
$url = $this->endpoint(rtrim($path, '/') . '/' . $providerOrderId);
|
||||
$token = $this->getToken();
|
||||
|
||||
try {
|
||||
$httpResponse = $this->http(15)->withToken($token)->get($url);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Montzar order status request failed', [
|
||||
'url' => $url,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$body = $this->decodeMontzarHttpBody($httpResponse);
|
||||
if (is_array($body) && ($body['message'] ?? null) === 'no authorised') {
|
||||
$token = $this->montzarLogin();
|
||||
$httpResponse = $this->http(15)->withToken($token)->get($url);
|
||||
$body = $this->decodeMontzarHttpBody($httpResponse);
|
||||
}
|
||||
|
||||
return is_array($body) ? $body : null;
|
||||
}
|
||||
|
||||
private function postToMontzar(string $url, string $token, array $payload)
|
||||
@@ -755,7 +805,7 @@ class MontzarService
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($item['sts']) && in_array((int) $item['sts'], [1, 2], true)) {
|
||||
if (isset($item['sts']) && in_array((int) $item['sts'], [1, 2, 4], true)) {
|
||||
return [
|
||||
'ok' => true,
|
||||
'provider_id' => !empty($item['id']) ? (int) $item['id'] : null,
|
||||
@@ -764,6 +814,14 @@ class MontzarService
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($body['pending']) || !empty($body[0]['pending'])) {
|
||||
return [
|
||||
'ok' => true,
|
||||
'provider_id' => $this->extractProviderOrderId($body),
|
||||
'error' => null,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => false,
|
||||
'provider_id' => null,
|
||||
@@ -777,16 +835,24 @@ class MontzarService
|
||||
return (int) $body['id'];
|
||||
}
|
||||
|
||||
if (!empty($body['order_id']) && is_numeric($body['order_id'])) {
|
||||
return (int) $body['order_id'];
|
||||
}
|
||||
|
||||
if (!empty($body['order']['id'])) {
|
||||
return (int) $body['order']['id'];
|
||||
}
|
||||
|
||||
foreach ([1, 2] as $index) {
|
||||
foreach ([0, 1, 2] as $index) {
|
||||
if (isset($body[$index]) && is_array($body[$index])) {
|
||||
if (!empty($body[$index]['id'])) {
|
||||
return (int) $body[$index]['id'];
|
||||
}
|
||||
|
||||
if (!empty($body[$index]['order_id']) && is_numeric($body[$index]['order_id'])) {
|
||||
return (int) $body[$index]['order_id'];
|
||||
}
|
||||
|
||||
if (!empty($body[$index]['order']['id'])) {
|
||||
return (int) $body[$index]['order']['id'];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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('orders')) {
|
||||
if (! Schema::hasColumn('orders', 'colleague_callback_payload')) {
|
||||
Schema::table('orders', function ($table) {
|
||||
$table->json('colleague_callback_payload')->nullable();
|
||||
});
|
||||
}
|
||||
if (! Schema::hasColumn('orders', 'colleague_last_poll_at')) {
|
||||
Schema::table('orders', function ($table) {
|
||||
$table->timestamp('colleague_last_poll_at')->nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (! Schema::hasTable('settings')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->insertSetting(
|
||||
'montzar_order_wait_callback',
|
||||
'true',
|
||||
'انتظار کالبک سفارش همکار',
|
||||
'روشن: سفارش همکار تا دریافت کالبک (یا استعلام مسیر وضعیت) در انتظار میماند و کیف ثبت نمیشود. خاموش: مثل قبل بلافاصله با پاسخ HTTP همکار قطعی میشود.',
|
||||
'switch'
|
||||
);
|
||||
$this->insertSetting(
|
||||
'montzar_order_status_path',
|
||||
'/server/customer/order',
|
||||
'مسیر استعلام وضعیت سفارش همکار',
|
||||
'مسیر GET روی سرور همکار (montzar_base_url). شماره سفارش همکار به انتها اضافه میشود؛ مثلاً /server/customer/order/40000.',
|
||||
'text'
|
||||
);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// settings and columns stay
|
||||
}
|
||||
|
||||
private function insertSetting(string $key, string $value, string $title, string $description, string $type): void
|
||||
{
|
||||
$exists = DB::table('settings')->where('key', $key)->exists();
|
||||
if ($exists) {
|
||||
return;
|
||||
}
|
||||
|
||||
$row = [
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'title_fa' => $title,
|
||||
'description' => $description,
|
||||
'can_edit' => 1,
|
||||
'is_active' => 1,
|
||||
];
|
||||
if (Schema::hasColumn('settings', 'type')) {
|
||||
$row['type'] = $type;
|
||||
}
|
||||
|
||||
DB::table('settings')->insert($row);
|
||||
}
|
||||
};
|
||||
@@ -254,6 +254,31 @@ PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- orders — payload/زمان استعلام کالبک همکار
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'orders') > 0
|
||||
AND (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'orders' AND COLUMN_NAME = 'colleague_callback_payload') = 0,
|
||||
'ALTER TABLE `orders` ADD COLUMN `colleague_callback_payload` JSON NULL',
|
||||
'SELECT ''skip orders.colleague_callback_payload'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @sql = IF(
|
||||
(SELECT COUNT(*) FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'orders') > 0
|
||||
AND (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'orders' AND COLUMN_NAME = 'colleague_last_poll_at') = 0,
|
||||
'ALTER TABLE `orders` ADD COLUMN `colleague_last_poll_at` TIMESTAMP NULL DEFAULT NULL',
|
||||
'SELECT ''skip orders.colleague_last_poll_at'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- withdraws — ستونهای اختیاری (روی بعضی سرورها دستی اضافه شده)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
@@ -346,6 +346,18 @@ INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `de
|
||||
SELECT 'montzar_open_sell', 'true', 'باز بودن فروش همکار', 1, 1, NULL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'montzar_open_sell' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'montzar_order_wait_callback', 'true', 'انتظار کالبک سفارش همکار', 1, 1,
|
||||
'روشن: سفارش همکار تا دریافت کالبک (یا استعلام مسیر وضعیت) در انتظار میماند و کیف ثبت نمیشود. خاموش: مثل قبل بلافاصله با پاسخ HTTP همکار قطعی میشود.',
|
||||
'switch'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'montzar_order_wait_callback' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`)
|
||||
SELECT 'montzar_order_status_path', '/server/customer/order', 'مسیر استعلام وضعیت سفارش همکار', 1, 1,
|
||||
'مسیر GET روی سرور همکار (montzar_base_url). شماره سفارش همکار به انتها اضافه میشود؛ مثلاً /server/customer/order/40000.',
|
||||
'text'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'montzar_order_status_path' LIMIT 1);
|
||||
|
||||
INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`)
|
||||
SELECT 'montzar_diff', '0', 'اختلاف قیمت همکار', 1, 1, 'سیستم پر میکند'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'montzar_diff' LIMIT 1);
|
||||
@@ -1357,7 +1369,7 @@ UPDATE `settings` SET `type` = 'switch' WHERE `key` IN (
|
||||
'sms_notif_jibit_admin', 'sms_notif_bank_card_admin', 'sms_notif_bank_card_customer',
|
||||
'sms_notif_fund_withdraw_admin', 'sms_notif_fund_withdraw_customer',
|
||||
'sms_notif_price_trigger', 'sms_notif_wallet_alert', 'sms_notif_support_admin',
|
||||
'montzar_closed', 'montzar_open_buy', 'montzar_open_sell',
|
||||
'montzar_closed', 'montzar_open_buy', 'montzar_open_sell', 'montzar_order_wait_callback',
|
||||
'payping_fin_sync_enabled', 'external_order_api_enabled', 'is_store', 'wss_enabled', 'tracking_systems_enabled',
|
||||
'register_referral_sms_to_referrer', 'register_referral_bonus_message',
|
||||
'referral_gift_on_first_order', 'referral_gift_buy_from_montzar',
|
||||
@@ -1386,6 +1398,7 @@ UPDATE `settings` SET `type` = 'text' WHERE `key` IN (
|
||||
'name_fa', 'name_en', 'app_url', 'bace_url', 'logo_login', 'logo_register', 'admin_url', 'title_pwa', 'title_fav',
|
||||
'server_url', 'key', 'main_product', 'zaarbox_api_url', 'zaarbox_price_key', 'chart_id',
|
||||
'etehadie_api_url', 'montzar_mobile', 'montzar_password', 'montzar_base_url',
|
||||
'montzar_order_status_path',
|
||||
'montzar_token', 'montzar_diff', 'montzar_live_price', 'montzar_mazaneh',
|
||||
'montzar_truly_buy_mazaneh', 'montzar_truly_sell_mazaneh',
|
||||
'montzar_account_zarin', 'montzar_account_tahesab', 'montzar_account_pargar',
|
||||
@@ -2384,7 +2397,8 @@ UPDATE `settings` SET `can_edit` = 1 WHERE `key` IN (
|
||||
'jibit_verification',
|
||||
'main_product_price',
|
||||
'min_first_buy', 'min_withdraw', 'min_deposit', 'minimum_sell',
|
||||
'montzar_mobile', 'montzar_open_buy', 'montzar_open_sell',
|
||||
'montzar_mobile', 'montzar_open_buy', 'montzar_open_sell', 'montzar_order_wait_callback',
|
||||
'montzar_order_status_path',
|
||||
'montzar_password', 'montzar_product_id',
|
||||
'montzar_account_zarin', 'montzar_account_tahesab', 'montzar_account_pargar',
|
||||
'open_register',
|
||||
|
||||
+9
-1
@@ -12,7 +12,8 @@ use App\Http\Controllers\Single\BankAccountController;
|
||||
use \App\Http\Controllers\Customer\FundController;
|
||||
use \App\Http\Controllers\pay\paypingController;
|
||||
use App\Http\Controllers\pay\WalletPaymentController;
|
||||
use \App\Http\Controllers\Single\SingleOrderController;
|
||||
use App\Http\Controllers\Single\SingleOrderController;
|
||||
use App\Http\Controllers\Customer\ColleagueOrderCallbackController;
|
||||
use App\Http\Controllers\Admin\ProductController as AdminProductController;
|
||||
use App\Http\Controllers\Customer\ProductController;
|
||||
use App\Http\Controllers\Admin\TahesabController;
|
||||
@@ -37,6 +38,9 @@ Route::post('/wallet/payment/webhook/{provider}', [WalletPaymentController::clas
|
||||
Route::get('/wallet/external-payment/return', [WalletPaymentController::class, 'returnLegacy']);
|
||||
Route::post('/wallet/external-payment/webhook', [WalletPaymentController::class, 'webhookLegacy']);
|
||||
|
||||
// کالبک وضعیت سفارش همکار — بدون لاگین مشتری
|
||||
Route::post('/colleague/order/callback/{order?}', [ColleagueOrderCallbackController::class, 'callback']);
|
||||
|
||||
$isStore = app(ConfigService::class)->isStore();
|
||||
|
||||
Route::middleware('validation')->group(function () {
|
||||
@@ -123,10 +127,14 @@ Route::middleware(['auth:customer', 'customer.active'])->group(function () use (
|
||||
|
||||
//-----------------order
|
||||
Route::post('order/table', [OrderController::class, 'table']);
|
||||
Route::get('order/pending', [SingleOrderController::class, 'pendingColleague']);
|
||||
Route::get('order/{id}/status', [SingleOrderController::class, 'colleagueStatus']);
|
||||
Route::post('order', [OrderController::class, 'store']);
|
||||
|
||||
//-----------------single order
|
||||
Route::post('single/order/table', [SingleOrderController::class, 'table']);
|
||||
Route::get('single/order/pending', [SingleOrderController::class, 'pendingColleague']);
|
||||
Route::get('single/order/{id}/status', [SingleOrderController::class, 'colleagueStatus']);
|
||||
// Route::post('single/order', [SingleOrderController::class, 'storeBackUp']);
|
||||
Route::post('single/order', [SingleOrderController::class, 'store']);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user