Files
boursecu_server/app/Services/ColleagueOrderCallbackService.php
T
mohmdsdgandCursor 00b23ac404 feat(tahesab,colleague): push app balance to Tahesab and always wait callback
Add admin decision to write app wallet onto Tahesab even when zero, always await colleague callback regardless of the wait switch, and SMS admins on colleague reject.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 22:30:56 +03:30

349 lines
9.9 KiB
PHP

<?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
{
// همیشه منتظر کالبک/استعلام همکار — سوییچ montzar_order_wait_callback دیگر مسیر «قطعی بلافاصله» را باز نمی‌کند
return app(ConfigService::class)->usesMontzarPricing();
}
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 refreshIfDue(Order $order): Order
{
if ((int) $order->sts !== 4) {
return $order;
}
if (!$this->isPollDue($order)) {
return $order;
}
try {
$this->pollOne($order);
} catch (\Throwable $e) {
Log::warning('Colleague pending poll failed', [
'order_id' => $order->id,
'error' => $e->getMessage(),
]);
$this->touchPollAt($order);
}
return Order::query()->find($order->id) ?? $order;
}
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'];
}
// جدول سفارش همکار همان sts مدل است (۲ موفق / ۳ ناموفق / ۴ هنوز باز).
// کالبک جداست: آن‌جا ۴ و ۶ یعنی رد.
if (in_array($sts, [3, 6], true)) {
return $this->rejectPending($order, $body);
}
if ($sts === 2) {
return $this->approvePending($order, $body);
}
return ['ok' => true, 'state' => 'wait', 'sts' => (int) $order->sts];
}
/**
* @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();
}
private function isPollDue(Order $order): bool
{
$cutoff = now()->subSeconds(self::POLL_SECONDS);
$last = $order->colleague_last_poll_at ?? null;
if ($last) {
return $last <= $cutoff;
}
$created = $order->created_at;
return !$created || $created <= $cutoff;
}
/**
* @param array<string, mixed> $body
*/
public function providerStsFromBody(array $body): ?int
{
$rows = [$body, $body['data'] ?? null, $body['order'] ?? null];
if (isset($body['rows']) && is_array($body['rows'])) {
foreach ($body['rows'] as $row) {
if (is_array($row)) {
$rows[] = $row;
}
}
}
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;
}
}