119 lines
4.6 KiB
PHP

<?php
namespace App\Http\Controllers\Integrations;
use App\Jobs\Business\ArchiveCourierQuoteSignature;
use App\Jobs\Business\ArchiveCourierAgreementSignature;
use App\Models\Business\CourierAgreementSignature;
use App\Models\Business\CourierQuoteSignature;
use App\Support\CourierAgreementSignatureService;
use App\Support\CourierQuoteSignatureService;
use App\Services\Esign\EsignConfiguration;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
class EsignCallbacks extends Controller
{
public function __invoke(Request $request, CourierQuoteSignatureService $service, CourierAgreementSignatureService $agreementService): JsonResponse
{
$configuration = EsignConfiguration::fromConfig();
if (! $configuration->isConfigured()) {
return response()->json(['code' => '503', 'msg' => 'not configured'], 503);
}
$appId = $configuration->appId();
$appSecret = $configuration->appSecret();
$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();
}
$payloadHash = hash('sha256', $rawBody);
$signatureRecord = $service->acceptCallback($signFlowId, $remoteStatus, $payloadHash);
if ($signatureRecord === null) {
$agreementRecord = $agreementService->acceptCallback($signFlowId, $remoteStatus, $payloadHash, [
'event_key' => hash('sha256', $timestamp . '|' . ($payload['action'] ?? '') . '|' . $signFlowId . '|' . $payloadHash),
'action' => (string) ($payload['action'] ?? 'SIGN_FLOW_COMPLETE'),
'timestamp' => $timestamp,
'raw_body' => $rawBody,
]);
if ($agreementRecord
&& $remoteStatus === '2'
&& $agreementService->autoArchive($agreementRecord)
&& $agreementRecord->status !== CourierAgreementSignature::STATUS_ARCHIVED) {
ArchiveCourierAgreementSignature::dispatch($agreementRecord->id);
}
}
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 = EsignConfiguration::fromConfig()->callbackToleranceSeconds();
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']);
}
}