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:
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user