96 lines
3.5 KiB
PHP

<?php
namespace App\Http\Controllers\Integrations;
use App\Jobs\Business\ArchiveCourierQuoteSignature;
use App\Models\Business\CourierQuoteSignature;
use App\Support\CourierQuoteSignatureService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
class EsignCallbacks extends Controller
{
public function __invoke(Request $request, CourierQuoteSignatureService $service): JsonResponse
{
$appId = trim((string) config('services.esign.app_id'));
$appSecret = trim((string) config('services.esign.app_secret'));
if ($appId === '' || $appSecret === '') {
return response()->json(['code' => '503', 'msg' => 'not configured'], 503);
}
$timestamp = trim((string) $request->header('X-Tsign-Open-TIMESTAMP'));
$algorithm = trim((string) $request->header('X-Tsign-Open-SIGNATURE-ALGORITHM'));
$callbackAppId = trim((string) $request->header('X-Tsign-Open-App-Id'));
$signature = strtolower(trim((string) $request->header('X-Tsign-Open-SIGNATURE')));
$rawBody = $request->getContent();
if (! ctype_digit($timestamp)
|| strcasecmp($algorithm, 'HmacSHA256') !== 0
|| ! hash_equals($appId, $callbackAppId)
|| $signature === ''
|| ! $this->timestampIsFresh($timestamp)) {
return response()->json(['code' => '401', 'msg' => 'invalid callback'], 401);
}
$queryValues = $request->query();
ksort($queryValues, SORT_STRING);
$data = $timestamp . collect($queryValues)->map(fn ($value): string => $this->queryValue($value))->implode('') . $rawBody;
$expected = strtolower(hash_hmac('sha256', $data, $appSecret));
if (! hash_equals($expected, $signature)) {
return response()->json(['code' => '401', 'msg' => 'invalid callback'], 401);
}
$payload = json_decode($rawBody, true);
if (! is_array($payload)) {
return response()->json(['code' => '400', 'msg' => 'invalid json'], 400);
}
if (($payload['action'] ?? null) !== 'SIGN_FLOW_COMPLETE') {
return $this->success();
}
$signFlowId = trim((string) ($payload['signFlowId'] ?? ''));
$remoteStatus = trim((string) ($payload['signFlowStatus'] ?? ''));
if ($signFlowId === '' || $remoteStatus === '') {
return $this->success();
}
$signatureRecord = $service->acceptCallback($signFlowId, $remoteStatus, hash('sha256', $rawBody));
if ($signatureRecord
&& $remoteStatus === '2'
&& $signatureRecord->status !== CourierQuoteSignature::STATUS_ARCHIVED) {
ArchiveCourierQuoteSignature::dispatchAfterResponse($signatureRecord->id);
}
return $this->success();
}
private function timestampIsFresh(string $timestamp): bool
{
$seconds = strlen($timestamp) >= 13 ? intdiv((int) $timestamp, 1000) : (int) $timestamp;
$tolerance = max(60, (int) config('services.esign.callback_tolerance_seconds', 900));
return abs(now()->timestamp - $seconds) <= $tolerance;
}
private function queryValue(mixed $value): string
{
if (is_array($value)) {
return collect($value)->map(fn ($item): string => $this->queryValue($item))->implode('');
}
return is_scalar($value) ? (string) $value : '';
}
private function success(): JsonResponse
{
return response()->json(['code' => '200', 'msg' => 'success']);
}
}