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>
349 lines
9.9 KiB
PHP
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;
|
|
}
|
|
}
|