config = $config ?? config('DigitMotor'); $this->authClient = $authClient ?? new DigitAuthClient($this->config); $this->logModel = new MotorApiLogModel(); helper('api'); } /** * POST to Digit executor (or arbitrary path under baseUrl). * * @throws DigitApiException */ public function post(string $path, array $payload, ?string $integrationId = null, ?int $quoteId = null): array { return $this->request('POST', $path, $payload, $integrationId, $quoteId, false); } /** * @throws DigitApiException */ protected function request( string $method, string $path, array $payload, ?string $integrationId, ?int $quoteId, bool $isRetry ): array { $token = $this->authClient->getToken($isRetry); $url = $this->config->baseUrl . $path; $headers = [ 'Content-Type: application/json', 'Authorization: Bearer ' . $token, ]; if ($integrationId) { $headers[] = 'integrationId: ' . $integrationId; } $started = microtime(true); $response = call_third_party_api($url, $method, $headers, $payload); $durationMs = (int) round((microtime(true) - $started) * 1000); $httpCode = (int) ($response['code'] ?? 0); $data = $response['data'] ?? []; if (is_string($data)) { $decoded = json_decode($data, true); $data = is_array($decoded) ? $decoded : ['raw' => $data]; } $digitCode = $this->extractDigitCode($data, $httpCode); $this->writeLog($quoteId, $integrationId, $path, $payload, $data, $httpCode, $digitCode, $durationMs); // Stale token — refresh once and retry if (!$isRetry && ($httpCode === 401 || (string) $digitCode === '401')) { $this->authClient->refreshToken(); return $this->request($method, $path, $payload, $integrationId, $quoteId, true); } if ($this->isHardFail($httpCode, $digitCode, $response['status'] ?? false, $data)) { $message = $this->extractMessage($data, $httpCode); throw new DigitApiException( $message, $digitCode, $httpCode, $data, null, $url, $this->redact($payload) ); } return is_array($data) ? $data : []; } protected function isHardFail(int $httpCode, $digitCode, bool $httpOk, $data): bool { $code = (string) $digitCode; if (in_array($code, ['403', '999', '400', '500'], true)) { return true; } if ($httpCode >= 400) { return true; } if (!$httpOk) { return true; } // Digit sometimes returns 200 with error object if (is_array($data) && isset($data['error']) && !isset($data['grossPremium']) && !isset($data['premium']) && !isset($data['quoteNumber'])) { $errCode = $data['error']['errorCode'] ?? $data['error']['code'] ?? $data['code'] ?? $data['statusCode'] ?? null; if ($errCode !== null && (string) $errCode !== '0' && strtoupper((string) $errCode) !== 'SUCCESS') { return true; } // validationMessages without a success payload if (!empty($data['error']['validationMessages'])) { return true; } } return false; } protected function extractDigitCode($data, int $httpCode) { if (!is_array($data)) { return $httpCode ?: null; } return $data['error']['errorCode'] ?? $data['error']['code'] ?? $data['code'] ?? $data['errorCode'] ?? $data['responseCode'] ?? $data['statusCode'] ?? ($httpCode >= 400 ? (string) $httpCode : null); } protected function extractMessage($data, int $httpCode): string { if (!is_array($data)) { return 'Digit API request failed (HTTP ' . $httpCode . ').'; } // Digit often returns validationMessages[] instead of a single message $validation = $data['error']['validationMessages'] ?? $data['validationMessages'] ?? null; if (is_array($validation) && $validation) { $parts = []; foreach ($validation as $item) { if (is_string($item) && trim($item) !== '') { $parts[] = trim($item); } elseif (is_array($item)) { $parts[] = trim((string) ($item['message'] ?? json_encode($item))); } } if ($parts) { return implode(' | ', $parts); } } $msg = $data['error']['message'] ?? $data['message'] ?? $data['errorMessage'] ?? $data['responseMessage'] ?? $data['statusMessage'] ?? null; if (is_array($msg)) { $msg = implode(' | ', array_map('strval', $msg)); } return $msg ?: 'Digit API request failed (HTTP ' . $httpCode . ').'; } protected function writeLog( ?int $quoteId, ?string $integrationId, string $endpoint, array $requestBody, $responseBody, int $httpStatus, $errorCode, int $durationMs ): void { try { $this->logModel->insert([ 'quote_id' => $quoteId, 'integration_id' => $integrationId, 'endpoint' => $endpoint, 'request_body' => json_encode($this->redact($requestBody)), 'response_body' => json_encode($this->redact(is_array($responseBody) ? $responseBody : ['raw' => $responseBody])), 'http_status' => $httpStatus, 'error_code' => $errorCode !== null ? (string) $errorCode : null, 'duration_ms' => $durationMs, ]); } catch (\Throwable $e) { log_message('error', 'DigitMotor API log write failed: ' . $e->getMessage()); } } protected function redact(array $data): array { $sensitive = ['password', 'access_token', 'refresh_token', 'Authorization', 'authorization']; $out = []; foreach ($data as $key => $value) { if (in_array((string) $key, $sensitive, true)) { $out[$key] = '***REDACTED***'; } elseif (is_array($value)) { $out[$key] = $this->redact($value); } else { $out[$key] = $value; } } return $out; } }