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
@@ -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
);
}
}
}