[ 'id' => self::METRIC_PULMO_GROUP, 'label' => 'Pulmosamrt Group', ], 'pulmosmart_dpi_pmdi' => [ 'id' => self::METRIC_PULMO_DPI_PMDI, 'label' => 'Pulmosmart DPI & pMDI', ], 'pulmosmart_neb' => [ 'id' => self::METRIC_PULMO_NEB, 'label' => 'Pulmosmart Neb', ], 'pulmosmart_g' => [ 'id' => self::METRIC_PULMO_G, 'label' => 'Pulmosmart G', ], 'budasure' => [ 'id' => self::METRIC_BUDASURE, 'label' => 'Budasure', ], 'viloresp_u' => [ 'id' => self::METRIC_VILORESP_U, 'label' => 'Viloresp U', ], ]; private const METRICS = [ self::METRIC_PBC => 'PBC Amt', self::METRIC_TOTAL => 'Total (Actual Value)', self::METRIC_PULMO_DPI_PMDI => 'Pulmosmart DPI & pMDI (Actual Value)', self::METRIC_PULMO_NEB => 'Pulmosmart Neb (Actual Value)', self::METRIC_PULMO_G => 'Pulmosmart G (Actual Value)', self::METRIC_PULMO_GROUP => 'Pulmosamrt Group (Actual Value)', self::METRIC_BUDASURE => 'Budasure (Actual Value)', self::METRIC_VILORESP_U => 'Viloresp U (Actual Value)', self::METRIC_PS_SHARE => '% PS Share', ]; private ?string $accessToken = null; private ?object $tokenUser = null; public function __construct() { $this->_service = 'reports'; $this->_module = 'pbs_doctor_analytics'; $this->_viewname = 'pbsdoctoranalyticsreport'; $this->_title = 'PBC Doctor Analytics'; $this->_base_url = '/pbsdoctoranalyticsreport'; } public function list(Request $request) { $this->bootstrapTokenSession($request); if ($request->hasSession()) { $request->session()->save(); } return redirect($this->buildUrl('/report', $request)); } public function report(Request $request) { $this->bootstrapTokenSession($request); $token = $this->assertPulmocareAccess($request); if ($request->hasSession()) { $request->session()->save(); } $payload = $this->buildReportPayload($request); $response = LambdaRequest::getrequest( $this->_service, $this->_module . '/doctor_units', $payload, $token, 300 ); $reportError = $this->resolveReportError($response); if ($reportError !== null) { if ($this->isAuthReportError($reportError)) { $this->forgetPersistedToken($request); abort(401, $reportError); } abort(502, $reportError); } $report = $response->data ?? (object) []; $allDataAccess = $this->hasAllDataAccess($this->tokenUser); $hierarchies = $allDataAccess ? $this->loadHierarchyOptions($token) : []; $brands = []; foreach (self::BRANDS as $key => $brand) { $brands[] = [ 'key' => $key, 'id' => $brand['id'], 'label' => $brand['label'], ]; } return $this->view('report')->with([ 'title' => $this->_title, 'base_url' => $this->_base_url, 'token' => $token, 'allDataAccess' => $allDataAccess, 'hierarchies' => $hierarchies, 'months' => $report->months ?? [], 'metrics' => $report->metrics ?? [], 'doctors' => $report->doctors ?? [], 'data' => $report->data ?? (object) [], 'brands' => $brands, 'defaultBrand' => self::DEFAULT_BRAND, 'metricIds' => [ 'pbc' => self::METRIC_PBC, 'total' => self::METRIC_TOTAL, 'pulmo' => self::METRIC_PULMO_GROUP, 'pct' => self::METRIC_PS_SHARE, ], ]); } protected function buildReportPayload(Request $request): array { $metricLabels = []; foreach (self::METRICS as $id => $label) { $metricLabels[$id] = $label; } $startMonth = trim((string) $request->query('start_month', self::REPORT_START)); if ($startMonth === '') { $startMonth = self::REPORT_START; } $payload = [ 'partition' => self::PARTITION, 'metric_ids' => json_encode(array_keys(self::METRICS)), 'metric_labels' => json_encode($metricLabels), 'start_month' => $startMonth, 'all' => '1', ]; $hierarchyIds = $request->query('hierarchy_ids'); if ($hierarchyIds !== null && $hierarchyIds !== '') { $payload['hierarchy_ids'] = is_array($hierarchyIds) ? json_encode(array_values($hierarchyIds)) : (string) $hierarchyIds; } return $payload; } protected function hasAllDataAccess(?object $user): bool { if (!$user) { return false; } $flag = $user->all_data_access ?? null; return $flag === true || $flag === 1 || $flag === '1' || (is_string($flag) && strtolower($flag) === 'true'); } protected function loadHierarchyOptions(string $token): array { $response = LambdaRequest::getrequest( 'ums', 'hierarchy/hierarchy_below_me', [ 'partition' => self::PARTITION, 'hierarchy_id' => 'root', ], $token ); if (!is_object($response) || !isset($response->data->hierarchies)) { return []; } $hierarchies = $response->data->hierarchies; if (!is_array($hierarchies)) { return []; } return array_values(array_filter($hierarchies, function ($hierarchy) { return is_object($hierarchy) && !empty($hierarchy->id); })); } protected function assertPulmocareAccess(Request $request): string { $token = $this->resolveAccessToken($request); if (!$token) { if ($request->filled('token')) { abort(403, 'Invalid or truncated access token. Please open the full link from the app.'); } abort(401, 'Access token is required. Open this report with ?token=... from the app.'); } $payload = $this->decodeTokenPayload($token); $user = is_object($payload) ? ($payload->user ?? null) : null; if (!$user || !$this->isPulmocareUser($user)) { abort(403, 'Access denied. A valid pulmocare token is required.'); } if ($this->isTokenPayloadExpired($payload)) { $this->forgetPersistedToken($request); abort(401, 'Your access token has expired. Please open a fresh link from the app.'); } $this->accessToken = $token; $this->tokenUser = $user; return $token; } protected function bootstrapTokenSession(Request $request): void { $token = $this->extractRequestToken($request); if ($token === '') { return; } $user = $this->getUserFromToken($token); if ($user && $this->isPulmocareUser($user)) { $payload = $this->decodeTokenPayload($token); if (!$this->isTokenPayloadExpired($payload)) { $this->persistAccessToken($request, $token); } } } protected function isPulmocareUser(object $user): bool { return ($user->domain ?? '') === 'pulmocare' || str_starts_with((string) ($user->partition ?? ''), self::PARTITION); } protected function normalizeRawToken(string $token): string { $token = trim($token); if (stripos($token, 'Bearer ') === 0) { $token = trim(substr($token, 7)); } return $token; } protected function resolveAccessToken(Request $request): ?string { $fromRequest = $this->extractRequestToken($request); if ($fromRequest !== '') { $payload = $this->decodeTokenPayload($fromRequest); $user = is_object($payload) ? ($payload->user ?? null) : null; if ($user && $this->isPulmocareUser($user) && !$this->isTokenPayloadExpired($payload)) { $this->persistAccessToken($request, $fromRequest); return $fromRequest; } } $storedToken = $this->loadPersistedToken($request); if ($storedToken !== '') { $payload = $this->decodeTokenPayload($storedToken); $user = is_object($payload) ? ($payload->user ?? null) : null; if ($user && $this->isPulmocareUser($user) && !$this->isTokenPayloadExpired($payload)) { return $storedToken; } $this->forgetPersistedToken($request); } return null; } protected function extractRequestToken(Request $request): string { $raw = $request->query('token', $request->input('token')); if ($raw === null || $raw === '') { $raw = $request->header('Authorization') ?? ''; } return $this->normalizeRawToken((string) $raw); } protected function persistAccessToken(Request $request, string $token): void { $request->session()->put('pbs_doctor_analytics_report_token', $token); $this->tokenStorage()->put($this->tokenStoragePath($request), $token); } protected function loadPersistedToken(Request $request): string { if ($request->session()->has('pbs_doctor_analytics_report_token')) { $sessionToken = $this->normalizeRawToken((string) $request->session()->get('pbs_doctor_analytics_report_token')); if ($sessionToken !== '') { return $sessionToken; } } $path = $this->tokenStoragePath($request); if ($this->tokenStorage()->exists($path)) { return $this->normalizeRawToken((string) $this->tokenStorage()->get($path)); } return ''; } protected function forgetPersistedToken(Request $request): void { $request->session()->forget('pbs_doctor_analytics_report_token'); $path = $this->tokenStoragePath($request); if ($this->tokenStorage()->exists($path)) { $this->tokenStorage()->delete($path); } } protected function tokenStoragePath(Request $request): string { return 'pbs_doctor_analytics_report_tokens/' . hash('sha256', $request->session()->getId()); } protected function tokenStorage() { return Storage::disk('local'); } protected function getUserFromToken(string $token): ?object { $payload = $this->decodeTokenPayload($token); if (!is_object($payload)) { return null; } return $payload->user ?? null; } protected function decodeTokenPayload(string $token): ?object { $token = $this->normalizeRawToken($token); $parts = explode('.', $token); if (count($parts) < 2) { return null; } $payloadSegment = strtr($parts[1], '-_', '+/'); $padding = strlen($payloadSegment) % 4; if ($padding > 0) { $payloadSegment .= str_repeat('=', 4 - $padding); } $payload = json_decode(base64_decode($payloadSegment)); return is_object($payload) ? $payload : null; } protected function isTokenPayloadExpired(?object $payload): bool { if (!is_object($payload) || !isset($payload->exp)) { return false; } return (int) $payload->exp < time(); } protected function resolveReportError($response): ?string { if (!is_object($response)) { return 'Report service returned an invalid response.'; } $status = strtolower((string) ($response->status ?? '')); $statusCode = (int) ($response->statusCode ?? 0); if ($status !== 'failed' && ($statusCode === 0 || $statusCode < 400)) { return null; } $message = trim((string) ($response->message ?? '')); if ($message === '') { $message = 'Failed to load report data.'; } if ($this->isAuthReportError($message) || $statusCode === 401 || $statusCode === 403) { return 'Your access token has expired or is invalid. Please open a fresh link from the app.'; } return $message; } protected function isAuthReportError(string $message): bool { $normalized = strtolower($message); return str_contains($normalized, 'unauthorized') || str_contains($normalized, 'expired') || str_contains($normalized, 'invalid token'); } protected function buildUrl(string $path, Request $request): string { $query = array_filter([ 'start_month' => $request->query('start_month'), ], function ($value) { return $value !== null && $value !== ''; }); $token = $this->extractRequestToken($request); if ($token === '') { $token = $this->loadPersistedToken($request); } if ($token !== '') { $query['token'] = $token; } $url = $this->_base_url . $path; if (!empty($query)) { $url .= '?' . http_build_query($query); } return $url; } }