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:
2026-09-02 15:55:03 +03:30
co-authored by Cursor
parent e9674d6571
commit 0b1e5c687b
13 changed files with 1093 additions and 105 deletions
@@ -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; namespace App\Console\Commands;
use App\Services\DatabaseLogPruneService;
use App\Services\PriceSnapshotService; use App\Services\PriceSnapshotService;
use Illuminate\Console\Command; use Illuminate\Console\Command;
@@ -9,11 +10,14 @@ class PrunePriceSnapshotsCommand extends Command
{ {
protected $signature = 'prices:prune'; 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)); $this->info(json_encode($stats, JSON_UNESCAPED_UNICODE));
return self::SUCCESS; return self::SUCCESS;
+8 -2
View File
@@ -31,10 +31,11 @@ class Kernel extends ConsoleKernel
->name('refresh_zaarbox_prices') ->name('refresh_zaarbox_prices')
->withoutOverlapping(); ->withoutOverlapping();
// پاک‌سازی و downsample تاریخچه — ساعت ۱۲ شب // پاک‌سازی و downsample تاریخچه قیمت + لاگ‌های حجیم (۳۱ روز) — ساعت ۱۲ شب
$schedule->command('prices:prune') $schedule->command('prices:prune')
->dailyAt('00:00') ->dailyAt('00:00')
->name('prune_price_snapshots'); ->name('prune_price_snapshots')
->withoutOverlapping();
// ترمیم اسناد ریالی جاافتادهٔ پرگار — سریع برای موارد تازه // ترمیم اسناد ریالی جاافتادهٔ پرگار — سریع برای موارد تازه
$schedule->command('pargar:requeue-wallet-documents --execute --days=14') $schedule->command('pargar:requeue-wallet-documents --execute --days=14')
@@ -52,6 +53,11 @@ class Kernel extends ConsoleKernel
->everyMinute() ->everyMinute()
->name('automations_run') ->name('automations_run')
->withoutOverlapping(50); ->withoutOverlapping(50);
$schedule->command('colleague:poll-pending-orders')
->everyMinute()
->name('colleague_poll_pending_orders')
->withoutOverlapping(58);
} }
protected function commands() 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\ReferralGiftService;
use App\Services\WalletBalanceService; use App\Services\WalletBalanceService;
use App\Services\WalletBroadcastService; use App\Services\WalletBroadcastService;
use App\Services\ColleagueOrderCallbackService;
use Illuminate\Support\Facades\Schema;
class SingleOrderController extends Controller class SingleOrderController extends Controller
@@ -115,7 +117,7 @@ class SingleOrderController extends Controller
throw new UnprocessableEntityHttpException('کیف پول یافت نشد.'); throw new UnprocessableEntityHttpException('کیف پول یافت نشد.');
} }
$response = []; $response = [];
$this->assertNoOpenColleagueOrder((int) ($data['customer_id'] ?? 0));
// Check if the user wants to transact with a product or non-product (gold) // 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'])) { 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); $response['sell'] = $this->processSellTransaction($data, $wallet, $wallet_interface);
} }
} }
// دفترکل داخلی همیشه ثبت شود — مستقل از اتصال به حسابداری بیرونی
if (isset($data['order'])) { if (!empty($data['_awaiting_callback']) && isset($data['order']) && $data['order'] instanceof Order) {
try { return $this->pendingColleagueStoreResponse($data['order']);
app(\App\Services\AccountingLedgerService::class)->recordTrade($data['order']);
} catch (\Throwable $e) {
report($e);
}
} }
if ($config->isTahesabAccounting()) { $this->runSuccessfulTradeAftermath($data, $customer, $wallet, $wallet_interface, $transactionType, $ownerType, $response);
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
);
}
return response()->json([$response, $data]); return response()->json([$response, $data]);
} }
@@ -362,6 +275,24 @@ class SingleOrderController extends Controller
$productId = (int) ($data['product_id'] ?? $abshode['id'] ?? 0); $productId = (int) ($data['product_id'] ?? $abshode['id'] ?? 0);
$usesProductWallet = $productId > 0 && !$config->isMainWalletProduct($productId); $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) { $this->runAfterProviderCover($order, $data, function () use (&$data, $wallet_interface, $productId, $usesProductWallet, $order) {
if ($usesProductWallet) { if ($usesProductWallet) {
new TransactionInterface($wallet_interface, [ new TransactionInterface($wallet_interface, [
@@ -642,6 +573,25 @@ class SingleOrderController extends Controller
$order = $this->openOrderThenCoverWithProvider($data); $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) { $this->runAfterProviderCover($order, $data, function () use (&$data, $wallet_interface, $wallet_interface_product, $productId, $usesProductWallet, $goldDebit, $order) {
if ($usesProductWallet) { if ($usesProductWallet) {
$this->normalizeProductWalletGold($wallet_interface_product); $this->normalizeProductWalletGold($wallet_interface_product);
@@ -1060,6 +1010,9 @@ class SingleOrderController extends Controller
$this->assertProviderOrderAccepted($data); $this->assertProviderOrderAccepted($data);
$this->finalizeProviderOrder($data); $this->finalizeProviderOrder($data);
$order->refresh(); $order->refresh();
if (app(ColleagueOrderCallbackService::class)->waitEnabled()) {
$data['_awaiting_callback'] = true;
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->markOrderFailed($order, $e->getMessage()); $this->markOrderFailed($order, $e->getMessage());
throw $e instanceof UnprocessableEntityHttpException throw $e instanceof UnprocessableEntityHttpException
@@ -2066,4 +2019,322 @@ class SingleOrderController extends Controller
return (float) ($data['payment_amount'] ?? $data['total_price'] ?? 0); 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
);
}
}
} }
+4
View File
@@ -40,10 +40,14 @@ class Order extends Model
'provider_id', 'provider_id',
'total_price_montzar', 'total_price_montzar',
'description', 'description',
'colleague_callback_payload',
'colleague_last_poll_at',
]; ];
protected $casts = [ protected $casts = [
'fee_breakdown' => 'array', 'fee_breakdown' => 'array',
'colleague_callback_payload' => 'array',
'colleague_last_poll_at' => 'datetime',
]; ];
static $STS = [ 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;
}
}
+140
View File
@@ -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];
}
}
+69 -3
View File
@@ -616,7 +616,7 @@ class MontzarService
? auth('customer')->user()->name ? auth('customer')->user()->name
: ($data['customer']['name'] ?? ''); : ($data['customer']['name'] ?? '');
return [ $payload = [
'mazaneh' => (int) $mazaneh, 'mazaneh' => (int) $mazaneh,
'amount' => (float) $data['amount'], 'amount' => (float) $data['amount'],
'description' => $this->config()->Config('name_fa') . ' - ' . $customerName, 'description' => $this->config()->Config('name_fa') . ' - ' . $customerName,
@@ -625,6 +625,56 @@ class MontzarService
'cash' => $cash, 'cash' => $cash,
'input_mode' => 'gold', '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) 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 [ return [
'ok' => true, 'ok' => true,
'provider_id' => !empty($item['id']) ? (int) $item['id'] : null, '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 [ return [
'ok' => false, 'ok' => false,
'provider_id' => null, 'provider_id' => null,
@@ -777,16 +835,24 @@ class MontzarService
return (int) $body['id']; return (int) $body['id'];
} }
if (!empty($body['order_id']) && is_numeric($body['order_id'])) {
return (int) $body['order_id'];
}
if (!empty($body['order']['id'])) { if (!empty($body['order']['id'])) {
return (int) $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 (isset($body[$index]) && is_array($body[$index])) {
if (!empty($body[$index]['id'])) { if (!empty($body[$index]['id'])) {
return (int) $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'])) { if (!empty($body[$index]['order']['id'])) {
return (int) $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);
}
};
+25
View File
@@ -254,6 +254,31 @@ PREPARE stmt FROM @sql;
EXECUTE stmt; EXECUTE stmt;
DEALLOCATE PREPARE 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 — ستون‌های اختیاری (روی بعضی سرورها دستی اضافه شده) -- withdraws — ستون‌های اختیاری (روی بعضی سرورها دستی اضافه شده)
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
+16 -2
View File
@@ -346,6 +346,18 @@ INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `de
SELECT 'montzar_open_sell', 'true', 'باز بودن فروش همکار', 1, 1, NULL SELECT 'montzar_open_sell', 'true', 'باز بودن فروش همکار', 1, 1, NULL
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'montzar_open_sell' LIMIT 1); 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`) INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`)
SELECT 'montzar_diff', '0', 'اختلاف قیمت همکار', 1, 1, 'سیستم پر می‌کند' SELECT 'montzar_diff', '0', 'اختلاف قیمت همکار', 1, 1, 'سیستم پر می‌کند'
WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'montzar_diff' LIMIT 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_jibit_admin', 'sms_notif_bank_card_admin', 'sms_notif_bank_card_customer',
'sms_notif_fund_withdraw_admin', 'sms_notif_fund_withdraw_customer', 'sms_notif_fund_withdraw_admin', 'sms_notif_fund_withdraw_customer',
'sms_notif_price_trigger', 'sms_notif_wallet_alert', 'sms_notif_support_admin', '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', 'payping_fin_sync_enabled', 'external_order_api_enabled', 'is_store', 'wss_enabled', 'tracking_systems_enabled',
'register_referral_sms_to_referrer', 'register_referral_bonus_message', 'register_referral_sms_to_referrer', 'register_referral_bonus_message',
'referral_gift_on_first_order', 'referral_gift_buy_from_montzar', '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', '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', 'server_url', 'key', 'main_product', 'zaarbox_api_url', 'zaarbox_price_key', 'chart_id',
'etehadie_api_url', 'montzar_mobile', 'montzar_password', 'montzar_base_url', 'etehadie_api_url', 'montzar_mobile', 'montzar_password', 'montzar_base_url',
'montzar_order_status_path',
'montzar_token', 'montzar_diff', 'montzar_live_price', 'montzar_mazaneh', 'montzar_token', 'montzar_diff', 'montzar_live_price', 'montzar_mazaneh',
'montzar_truly_buy_mazaneh', 'montzar_truly_sell_mazaneh', 'montzar_truly_buy_mazaneh', 'montzar_truly_sell_mazaneh',
'montzar_account_zarin', 'montzar_account_tahesab', 'montzar_account_pargar', 'montzar_account_zarin', 'montzar_account_tahesab', 'montzar_account_pargar',
@@ -2384,7 +2397,8 @@ UPDATE `settings` SET `can_edit` = 1 WHERE `key` IN (
'jibit_verification', 'jibit_verification',
'main_product_price', 'main_product_price',
'min_first_buy', 'min_withdraw', 'min_deposit', 'minimum_sell', '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_password', 'montzar_product_id',
'montzar_account_zarin', 'montzar_account_tahesab', 'montzar_account_pargar', 'montzar_account_zarin', 'montzar_account_tahesab', 'montzar_account_pargar',
'open_register', 'open_register',
+9 -1
View File
@@ -12,7 +12,8 @@ use App\Http\Controllers\Single\BankAccountController;
use \App\Http\Controllers\Customer\FundController; use \App\Http\Controllers\Customer\FundController;
use \App\Http\Controllers\pay\paypingController; use \App\Http\Controllers\pay\paypingController;
use App\Http\Controllers\pay\WalletPaymentController; 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\Admin\ProductController as AdminProductController;
use App\Http\Controllers\Customer\ProductController; use App\Http\Controllers\Customer\ProductController;
use App\Http\Controllers\Admin\TahesabController; 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::get('/wallet/external-payment/return', [WalletPaymentController::class, 'returnLegacy']);
Route::post('/wallet/external-payment/webhook', [WalletPaymentController::class, 'webhookLegacy']); Route::post('/wallet/external-payment/webhook', [WalletPaymentController::class, 'webhookLegacy']);
// کالبک وضعیت سفارش همکار — بدون لاگین مشتری
Route::post('/colleague/order/callback/{order?}', [ColleagueOrderCallbackController::class, 'callback']);
$isStore = app(ConfigService::class)->isStore(); $isStore = app(ConfigService::class)->isStore();
Route::middleware('validation')->group(function () { Route::middleware('validation')->group(function () {
@@ -123,10 +127,14 @@ Route::middleware(['auth:customer', 'customer.active'])->group(function () use (
//-----------------order //-----------------order
Route::post('order/table', [OrderController::class, 'table']); 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']); Route::post('order', [OrderController::class, 'store']);
//-----------------single order //-----------------single order
Route::post('single/order/table', [SingleOrderController::class, 'table']); 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, 'storeBackUp']);
Route::post('single/order', [SingleOrderController::class, 'store']); Route::post('single/order', [SingleOrderController::class, 'store']);