{"openapi":"3.0.0","paths":{"/":{"get":{"operationId":"RootController_root","parameters":[],"responses":{"200":{"description":""}},"tags":["Root"]}},"/api/health":{"get":{"operationId":"HealthController_getHealth","parameters":[],"responses":{"200":{"description":""}},"summary":"Liveness probe (no database check in Phase 1)","tags":["health"]}},"/api/auth/register":{"post":{"operationId":"AuthController_register","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterDto"}}}},"responses":{"200":{"description":"User registered successfully.","content":{"application/json":{"schema":{"example":{"success":true,"data":{"user":{"id":"1","name":"Ada","email":"ada@example.com"}},"message":"User registered successfully."}}}}},"404":{"description":"Validation Error envelope (Laravel quirk).","content":{"application/json":{"schema":{"example":{"success":false,"message":"Validation Error.","data":{"email":["The email has already been taken."]}}}}}}},"summary":"Register a new user (Laravel parity: validation errors → 404).","tags":["auth"]}},"/api/auth/login":{"post":{"operationId":"AuthController_login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"success":{"type":"boolean","example":true},"message":{"type":"string","example":"User logged in successfully."},"data":{"$ref":"#/components/schemas/TokenPayloadDto"}}}}}},"401":{"description":"Invalid credentials.","content":{"application/json":{"schema":{"example":{"success":false,"message":"Invalid credentials","data":{"error":"Invalid email or password. Please try again."}}}}}},"422":{"description":"Validation Error.","content":{"application/json":{"schema":{"example":{"success":false,"message":"Validation Error.","data":{"email":["..."]}}}}}},"500":{"description":"Unexpected server error."}},"summary":"Login with email/password; returns JWT access payload in `data`.","tags":["auth"]}},"/api/auth/logout":{"post":{"operationId":"AuthController_logout","parameters":[{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"example":{"success":true,"data":[],"message":"Successfully logged out."}}}}},"401":{"description":"Missing/invalid JWT","content":{"application/json":{"schema":{"example":{"success":false,"message":"Unauthorized","data":{"error":"No user is currently logged in."}}}}}}},"security":[{"JWT":[]}],"summary":"Logout (invalidates token when blacklist is enabled).","tags":["auth"]}},"/api/auth/refresh":{"post":{"operationId":"AuthController_refresh","parameters":[{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"success":{"type":"boolean"},"message":{"type":"string","example":"Token refreshed successfully."},"data":{"$ref":"#/components/schemas/TokenPayloadDto"}}}}}},"401":{"description":"Invalid / expired refresh window / blacklisted token."}},"security":[{"JWT":[]}],"summary":"Refresh JWT (Bearer). Allows expired access token within refresh TTL from `iat`.","tags":["auth"]}},"/api/auth/profile":{"get":{"operationId":"AuthController_getProfile","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"properties":{"success":{"type":"boolean"},"message":{"type":"string","example":"User profile fetched successfully."},"data":{"$ref":"#/components/schemas/UserPublicDto"}}}}}},"401":{"description":"","content":{"application/json":{"schema":{"example":{"success":false,"message":"Unauthorized","data":{"error":"User not authenticated."}}}}}}},"security":[{"JWT":[]}],"summary":"Current user profile (`data` is raw user object, Laravel R4.6).","tags":["auth"]}},"/api/user":{"get":{"description":"Send the same **JWT access token** as for other protected endpoints (`Authorization: Bearer <token>`). Response body is the public user object at the **root** (not wrapped in `data`). Use `GET /api/auth/profile` if you need the Laravel-style `sendResponse` envelope.","operationId":"UserController_getCurrentUser","parameters":[],"responses":{"200":{"description":"Unwrapped public user (no nested `data` wrapper).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPublicDto"}}}},"401":{"description":"Not authenticated (same `sendError` envelope as `JwtAuthGuard` + profile)."}},"security":[{"JWT":[]}],"summary":"Current user (R4.1) — unwrapped user JSON (JWT Bearer)","tags":["user"]}},"/api/projects/public":{"get":{"operationId":"PortfolioCmsController_findPublic","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PortfolioProjectSwaggerDto"}}}}}}}},"summary":"Public active portfolio projects (home page).","tags":["portfolio-projects"]}},"/api/projects/manage":{"get":{"operationId":"PortfolioCmsController_findAllAdmin","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PortfolioProjectSwaggerDto"}}}}}}}},"security":[{"JWT":[]}],"summary":"List all portfolio projects for admin (includes inactive). Sorted by displayOrder, createdAt.","tags":["portfolio-projects"]}},"/api/projects/{id}":{"get":{"operationId":"PortfolioCmsController_findOne","parameters":[{"name":"id","required":true,"in":"path","description":"Numeric portfolio project id.","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PortfolioProjectSwaggerDto"}}}}}}},"security":[{"JWT":[]}],"summary":"Get one portfolio project by id.","tags":["portfolio-projects"]},"patch":{"operationId":"PortfolioCmsController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Numeric portfolio project id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePortfolioProjectDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"data":{"$ref":"#/components/schemas/PortfolioProjectSwaggerDto"}}}}}}},"security":[{"JWT":[]}],"summary":"Update portfolio project.","tags":["portfolio-projects"]},"delete":{"operationId":"PortfolioCmsController_remove","parameters":[{"name":"id","required":true,"in":"path","description":"Numeric portfolio project id.","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}}},"security":[{"JWT":[]}],"summary":"Soft-delete portfolio project.","tags":["portfolio-projects"]}},"/api/projects/upload":{"post":{"operationId":"PortfolioCmsController_upload","parameters":[{"name":"frame","required":false,"in":"query","description":"Required for `purpose=media` (video or image). Frame 1 = landing preview; frame 2 = detail popup. Defaults to `1` if omitted.","schema":{"enum":["1","2"],"type":"string"}},{"name":"purpose","required":false,"in":"query","description":"For images: `poster` stores under projects/posters (ignores `frame`). Default `media` uses frame paths.","schema":{"enum":["media","poster"],"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"Image or video file."}}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"},"filename":{"type":"string"},"mimeType":{"type":"string"},"size":{"type":"number"},"type":{"type":"string","enum":["image","video"]}}}}}}},"security":[{"JWT":[]}],"summary":"Upload image or video for portfolio media. Media files use `frame`: 1 (landing card) or 2 (detail modal), under …/projects/images/frame-{n}/ or …/videos/frame-{n}/. Posters stay under …/projects/posters.","tags":["portfolio-projects"]}},"/api/projects":{"post":{"operationId":"PortfolioCmsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePortfolioProjectDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"data":{"$ref":"#/components/schemas/PortfolioProjectSwaggerDto"}}}}}}},"security":[{"JWT":[]}],"summary":"Create portfolio project.","tags":["portfolio-projects"]},"get":{"operationId":"ProjectsController_findAll","parameters":[{"name":"search","required":false,"in":"query","description":"Substring match on `title`.","schema":{"type":"string"}},{"name":"technologies","required":false,"in":"query","description":"JSON array contains this string value.","schema":{"type":"string"}},{"name":"service","required":false,"in":"query","description":"ILIKE match on `service` text (Laravel JSON filter parity).","schema":{"type":"string"}},{"name":"sort_by","required":false,"in":"query","description":"Allowlisted: title, id, created_at, updated_at, short_description (unknown → title).","schema":{"type":"string"}},{"name":"order","required":false,"in":"query","description":"`asc` or `desc` for sorting (default asc). Response `filters.order` defaults to `desc` when omitted (R4.7).","schema":{"type":"string","enum":["asc","desc"]}},{"name":"page","required":false,"in":"query","description":"1-based page (default 1).","schema":{"minimum":1,"default":1,"type":"number"}}],"responses":{"200":{"description":"Laravel-style pagination envelope","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/LaravelProjectSwaggerDto"}},"pagination_metadata":{"type":"object","properties":{"current_page":{"type":"number"},"per_page":{"type":"number"},"total_pages":{"type":"number"},"total_items":{"type":"number"}}},"links":{"type":"object","properties":{"first_page_url":{"type":"string"},"last_page_url":{"type":"string"},"next_page_url":{"type":"string","nullable":true},"prev_page_url":{"type":"string","nullable":true}}},"filters":{"type":"object","properties":{"search":{"type":"string","nullable":true},"technologies":{"type":"string","nullable":true},"service":{"type":"string","nullable":true},"sort_by":{"type":"string"},"order":{"type":"string"}}}}}}}}},"summary":"Paginated project listing (public, R4.7).","tags":["projects"]}},"/api/projects/create":{"post":{"operationId":"ProjectsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Project created successfully."},"project":{"$ref":"#/components/schemas/LaravelProjectSwaggerDto"}}}}}}},"security":[{"JWT":[]}],"summary":"Create project (JWT, R4.8).","tags":["projects"]}},"/api/projects/{project}/update":{"put":{"operationId":"ProjectsController_update","parameters":[{"name":"project","required":true,"in":"path","description":"Project id (Laravel route model binding).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Project updated successfully."},"project":{"$ref":"#/components/schemas/LaravelProjectSwaggerDto"}}}}}}},"security":[{"JWT":[]}],"summary":"Update project (JWT, R4.9).","tags":["projects"]}},"/api/projects/{project}/delete":{"delete":{"operationId":"ProjectsController_remove","parameters":[{"name":"project","required":true,"in":"path","description":"Project id.","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Project deleted successfully."}}}}}}},"security":[{"JWT":[]}],"summary":"Delete project (JWT, R4.10).","tags":["projects"]}},"/api/company/public-projects":{"get":{"operationId":"CompanyProjectsController_publicProjects","parameters":[{"name":"search","required":false,"in":"query","description":"ILIKE on `service` (R4.23).","schema":{"type":"string"}},{"name":"technologies","required":false,"in":"query","schema":{"type":"string"}},{"name":"service","required":false,"in":"query","description":"ILIKE on `service` text (parity vs Laravel JSON contains).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Public projects fetched successfully."},"data":{"type":"array","items":{"$ref":"#/components/schemas/LaravelProjectSwaggerDto"}}}}}}}},"summary":"Non-paginated public projects (R4.23).","tags":["company"]}},"/api/upload-image":{"post":{"operationId":"MediaController_uploadImage","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["image"],"properties":{"image":{"type":"string","format":"binary","description":"Image file (jpg, jpeg, png, gif, svg)."}}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Image uploaded successfully"},"image_url":{"type":"string","format":"uri","example":"https://storage.googleapis.com/your-bucket/company-site/images/image_123.jpg (or local: http://localhost:3000/uploads/images/image_123.jpg when GCS is unset)"}}}}}},"400":{"description":"Validation failure (R4.26): `{ error: <validator-like object> }`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaValidationErrorSchema"}}}},"401":{"description":"Missing or invalid JWT."}},"security":[{"JWT":[]}],"summary":"Upload image (JWT, R4.26).","tags":["media"]}},"/api/upload-video":{"post":{"operationId":"MediaController_uploadVideo","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["video"],"properties":{"video":{"type":"string","format":"binary","description":"Video file (allowed MIME types per blueprint)."}}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Video uploaded successfully"},"video_url":{"type":"string","format":"uri","example":"https://storage.googleapis.com/your-bucket/company-site/videos/video_123.mp4 (or local: http://localhost:3000/uploads/videos/... when GCS is unset)"}}}}}},"400":{"description":"Validation failure (R4.27): `{ error: ... }`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaValidationErrorSchema"}}}},"401":{"description":"Missing or invalid JWT."}},"security":[{"JWT":[]}],"summary":"Upload video (JWT, R4.27).","tags":["media"]}},"/api/services":{"get":{"operationId":"ServicesController_findAll","parameters":[{"name":"search","required":false,"in":"query","description":"Substring match on `title` (ILIKE).","schema":{"type":"string"}},{"name":"sort_by","required":false,"in":"query","description":"Allowlisted: title, id, slug, created_at, updated_at, description (unknown → title).","schema":{"type":"string"}},{"name":"order","required":false,"in":"query","description":"`asc` or `desc` (default asc). `filters.order` defaults to `desc` when omitted (R4.11).","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"minimum":1,"default":1,"type":"number"}}],"responses":{"200":{"description":"Laravel-style pagination envelope","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/LaravelServiceSwaggerDto"}},"pagination_metadata":{"type":"object","properties":{"current_page":{"type":"number"},"per_page":{"type":"number"},"total_pages":{"type":"number"},"total_items":{"type":"number"}}},"links":{"type":"object","properties":{"first_page_url":{"type":"string"},"last_page_url":{"type":"string"},"next_page_url":{"type":"string","nullable":true},"prev_page_url":{"type":"string","nullable":true}}},"filters":{"type":"object","properties":{"search":{"type":"string","nullable":true},"sort_by":{"type":"string"},"order":{"type":"string"}}}}}}}}},"summary":"Paginated services listing (public, R4.11).","tags":["services"]}},"/api/services/create":{"post":{"operationId":"ServicesController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateServiceDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Service created successfully."},"data":{"$ref":"#/components/schemas/LaravelServiceSwaggerDto"}}}}}}},"security":[{"JWT":[]}],"summary":"Create service (JWT, R4.12).","tags":["services"]}},"/api/services/{id}/update":{"put":{"operationId":"ServicesController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Numeric service id.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateServiceDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Service updated successfully."},"data":{"$ref":"#/components/schemas/LaravelServiceSwaggerDto"}}}}}}},"security":[{"JWT":[]}],"summary":"Update service (JWT, R4.13).","tags":["services"]}},"/api/services/{id}/delete":{"delete":{"operationId":"ServicesController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"message":{"type":"string","example":"Service Deleted Successfully !"}}}}}}},"security":[{"JWT":[]}],"summary":"Delete service (JWT, R4.14).","tags":["services"]}},"/api/company/public-services":{"get":{"operationId":"CompanyServicesController_publicServices","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Public services fetched successfully."},"data":{"type":"array","items":{"$ref":"#/components/schemas/LaravelServiceSwaggerDto"}}}}}}}},"summary":"All services, newest first (`Service::latest()->get()`, R4.24).","tags":["company"]}},"/api/blogs":{"get":{"operationId":"BlogsController_findAll","parameters":[{"name":"search","required":false,"in":"query","description":"Substring match on `title` (ILIKE).","schema":{"type":"string"}},{"name":"tags","required":false,"in":"query","description":"Tag string contained in the JSON `tags` array (Laravel `whereJsonContains` parity).","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"minimum":1,"type":"number"}}],"responses":{"200":{"description":"Laravel-style pagination envelope; `filters.order` is always `desc` (R4.15).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/LaravelBlogSwaggerDto"}},"pagination_metadata":{"type":"object","properties":{"current_page":{"type":"number"},"per_page":{"type":"number"},"total_pages":{"type":"number"},"total_items":{"type":"number"}}},"links":{"type":"object","properties":{"first_page_url":{"type":"string"},"last_page_url":{"type":"string"},"next_page_url":{"type":"string","nullable":true},"prev_page_url":{"type":"string","nullable":true}}},"filters":{"type":"object","properties":{"search":{"type":"string","nullable":true},"tags":{"type":"string","nullable":true},"order":{"type":"string","example":"desc"}}}}}}}}},"summary":"Paginated blogs listing (public, R4.15).","tags":["blogs"]}},"/api/blogs/create":{"post":{"operationId":"BlogsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBlogDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Blog created successfully."},"data":{"$ref":"#/components/schemas/LaravelBlogSwaggerDto"}}}}}},"500":{"description":"R4.16: Laravel catches any store exception and returns this body.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"Failed to create blog."}}}}}}},"security":[{"JWT":[]}],"summary":"Create blog (JWT, R4.16).","tags":["blogs"]}},"/api/blogs/{blog}/update":{"put":{"operationId":"BlogsController_update","parameters":[{"name":"blog","required":true,"in":"path","description":"Numeric blog id (Laravel route model binding).","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBlogDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Blog updated successfully."},"data":{"$ref":"#/components/schemas/LaravelBlogSwaggerDto"}}}}}}},"security":[{"JWT":[]}],"summary":"Update blog (JWT, R4.17).","tags":["blogs"]}},"/api/blogs/{blog}/delete":{"delete":{"operationId":"BlogsController_remove","parameters":[{"name":"blog","required":true,"in":"path","description":"Numeric blog id.","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Blog deleted successfully."}}}}}}},"security":[{"JWT":[]}],"summary":"Delete blog (JWT, R4.18).","tags":["blogs"]}},"/api/company/public-blogs":{"get":{"description":"Laravel references `technologies` / `service` filters on this route without matching `blogs` columns — those are not implemented here (see service TODO).","operationId":"CompanyBlogsController_publicBlogs","parameters":[{"name":"search","required":false,"in":"query","description":"ILIKE match on `title`.","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"minimum":1,"type":"number"}}],"responses":{"200":{"description":"`data` is a Laravel LengthAwarePaginator-style object.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Public blogs fetched successfully."},"data":{"type":"object","properties":{"current_page":{"type":"number"},"data":{"type":"array","items":{"$ref":"#/components/schemas/LaravelBlogSwaggerDto"}},"first_page_url":{"type":"string"},"from":{"type":"number","nullable":true},"last_page":{"type":"number"},"last_page_url":{"type":"string"},"links":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string","nullable":true},"label":{"type":"string"},"active":{"type":"boolean"}}}},"next_page_url":{"type":"string","nullable":true},"path":{"type":"string"},"per_page":{"type":"number"},"prev_page_url":{"type":"string","nullable":true},"to":{"type":"number","nullable":true},"total":{"type":"number"}}}}}}}}},"summary":"Public paginated blogs (R4.25).","tags":["company"]}},"/api/company/public-blogs/{id}":{"get":{"operationId":"CompanyBlogsController_publicBlogById","parameters":[{"name":"id","required":true,"in":"path","description":"Numeric blog id.","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LaravelBlogSwaggerDto"}}}},"404":{"description":"Missing or invalid id","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","example":"Blog not found."}}}}}}},"summary":"Public blog by id (R4.25).","tags":["company"]}},"/api/service-types":{"get":{"operationId":"ServiceTypesController_findAll","parameters":[{"name":"search","required":false,"in":"query","description":"Substring match on `title` (ILIKE).","schema":{"type":"string"}},{"name":"page","required":false,"in":"query","schema":{"minimum":1,"type":"number"}}],"responses":{"200":{"description":"Laravel-style pagination envelope; `filters` contains only `search`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/LaravelServiceTypeSwaggerDto"}},"pagination_metadata":{"type":"object","properties":{"current_page":{"type":"number"},"per_page":{"type":"number"},"total_pages":{"type":"number"},"total_items":{"type":"number"}}},"links":{"type":"object","properties":{"first_page_url":{"type":"string"},"last_page_url":{"type":"string"},"next_page_url":{"type":"string","nullable":true},"prev_page_url":{"type":"string","nullable":true}}},"filters":{"type":"object","properties":{"search":{"type":"string","nullable":true}}}}}}}}},"summary":"Paginated service types listing (public, R4.19).","tags":["service-types"]}},"/api/service-types/create":{"post":{"description":"Returns **201** with the raw entity JSON at the root (no `{ message, data }` wrapper).","operationId":"ServiceTypesController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateServiceTypeDto"}}}},"responses":{"201":{"description":"Raw `ServiceType` JSON (snake_case timestamps).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LaravelServiceTypeSwaggerDto"}}}}},"security":[{"JWT":[]}],"summary":"Create service type (JWT, R4.20).","tags":["service-types"]}},"/api/service-types/{id}/update":{"put":{"description":"No request validation (Laravel parity). Only `title`, `slug`, `description`, and `icons` keys that are **present** on the JSON object are applied.","operationId":"ServiceTypesController_update","parameters":[{"name":"id","required":true,"in":"path","description":"Numeric service type id.","schema":{"type":"string"}}],"requestBody":{"required":true,"description":"Any subset of fields; extra properties are ignored by the service layer.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateServiceTypeDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"message":{"type":"string","example":"ServiceType updated successfully."},"data":{"$ref":"#/components/schemas/LaravelServiceTypeSwaggerDto"}}}}}}},"security":[{"JWT":[]}],"summary":"Partial update (JWT, R4.21).","tags":["service-types"]}},"/api/service-types/{id}/delete":{"delete":{"operationId":"ServiceTypesController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"message":{"type":"string","example":"ServiceType deleted"}}}}}}},"security":[{"JWT":[]}],"summary":"Delete service type (JWT, R4.22).","tags":["service-types"]}}},"info":{"title":"Company Site API","description":"**Authentication:** HTTP **Bearer JWT** is the only supported scheme. Obtain access and refresh tokens via `POST /api/auth/login` or `POST /api/auth/register`, then send `Authorization: Bearer <access_token>` on protected routes (including `GET /api/user`). **Sanctum** and Laravel personal access tokens are **not** implemented. Migrated modules: projects, services, blogs, service-types, media, company public routes. Contract details: `docs/NEST_PARITY_AUDIT_REPORT.md`.","version":"1.0.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"JWT":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"Access token from `POST /api/auth/login` or `POST /api/auth/register` (field `data.access_token`). Refresh via `POST /api/auth/refresh`."}},"schemas":{"TokenPayloadDto":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string","example":"bearer"},"expires_in":{"type":"number","description":"Access TTL in seconds (Laravel `getTTL() * 60`)."},"expires_at":{"type":"string","description":"Access expiry in `Y-m-d H:i:s` (UTC). TODO: align with Laravel app timezone if needed.","example":"2026-05-23 12:00:00"}},"required":["access_token","token_type","expires_in","expires_at"]},"UserPublicDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"email_verified_at":{"type":"object","nullable":true},"created_at":{"type":"string"},"updated_at":{"type":"string"},"otp":{"type":"object","nullable":true},"otp_expires_at":{"type":"object","nullable":true}},"required":["id","name","email","email_verified_at","created_at","updated_at","otp","otp_expires_at"]},"RegisterDto":{"type":"object","properties":{"name":{"type":"string","example":"Ada Lovelace"},"email":{"type":"string","example":"ada@example.com"},"password":{"type":"string","example":"secret12","minLength":6}},"required":["name","email","password"]},"LoginDto":{"type":"object","properties":{"email":{"type":"string","example":"ada@example.com"},"password":{"type":"string"}},"required":["email","password"]},"PortfolioProjectSwaggerDto":{"type":"object","properties":{"id":{"type":"string","example":"1"},"slug":{"type":"string"},"title":{"type":"string"},"type":{"type":"string","enum":["video","image"]},"frameOneUrl":{"type":"string","description":"Landing page / card preview media URL."},"frameTwoUrl":{"type":"string","description":"Detail modal primary media URL."},"mediaUrl":{"type":"object","description":"Legacy single media URL (fallback same as frame 1 when present).","deprecated":true},"posterUrl":{"type":"object"},"category":{"type":"object"},"description":{"type":"object"},"shortDescription":{"type":"object"},"tags":{"type":"array","items":{"type":"string"}},"displayOrder":{"type":"number"},"isActive":{"type":"boolean"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"deletedAt":{"type":"object"}},"required":["id","slug","title","type","frameOneUrl","frameTwoUrl","tags","displayOrder","isActive","createdAt","updatedAt"]},"CreatePortfolioProjectDto":{"type":"object","properties":{"title":{"type":"string","example":"Showreel 2026"},"slug":{"type":"string","description":"URL slug; generated from title when omitted.","example":"showreel-2026"},"type":{"type":"string","enum":["video","image"]},"frameOneUrl":{"type":"string","description":"Public URL for landing page / grid card media (frame 1)."},"frameTwoUrl":{"type":"string","description":"Public URL for detail modal media (frame 2)."},"mediaUrl":{"type":"string","description":"Deprecated single media URL; use frameOneUrl and frameTwoUrl.","deprecated":true},"posterUrl":{"type":"string"},"category":{"type":"string"},"description":{"type":"string"},"shortDescription":{"type":"string","description":"Card subtitle line (optional)."},"tags":{"example":["cg","trailer"],"type":"array","items":{"type":"string"}},"displayOrder":{"type":"number","default":0},"isActive":{"type":"boolean","default":true}},"required":["title","type","frameOneUrl","frameTwoUrl"]},"UpdatePortfolioProjectDto":{"type":"object","properties":{"title":{"type":"string","example":"Showreel 2026"},"slug":{"type":"string","description":"URL slug; generated from title when omitted.","example":"showreel-2026"},"type":{"type":"string","enum":["video","image"]},"frameOneUrl":{"type":"string","description":"Public URL for landing page / grid card media (frame 1)."},"frameTwoUrl":{"type":"string","description":"Public URL for detail modal media (frame 2)."},"mediaUrl":{"type":"string","description":"Deprecated single media URL; use frameOneUrl and frameTwoUrl.","deprecated":true},"posterUrl":{"type":"string"},"category":{"type":"string"},"description":{"type":"string"},"shortDescription":{"type":"string","description":"Card subtitle line (optional)."},"tags":{"example":["cg","trailer"],"type":"array","items":{"type":"string"}},"displayOrder":{"type":"number","default":0},"isActive":{"type":"boolean","default":true}}},"LaravelProjectSwaggerDto":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"short_description":{"type":"object","nullable":true},"technologies":{"type":"array","items":{"type":"string"}},"service":{"type":"object","nullable":true},"thumbnail":{"type":"object","nullable":true},"video":{"type":"object","nullable":true},"about":{"type":"object","nullable":true},"problem":{"type":"object","nullable":true},"solutions":{"type":"object","nullable":true},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","title","short_description","technologies","service","thumbnail","video","about","problem","solutions","created_at","updated_at"]},"CreateProjectDto":{"type":"object","properties":{"title":{"type":"string","description":"Required; max 255 characters."},"short_description":{"type":"object","nullable":true},"technologies":{"nullable":true,"type":"array","items":{"type":"string"}},"service":{"type":"object","nullable":true},"thumbnail":{"type":"object","nullable":true},"video":{"type":"object","nullable":true},"about":{"type":"object","nullable":true},"problem":{"type":"object","nullable":true},"solutions":{"type":"object","nullable":true}},"required":["title"]},"UpdateProjectDto":{"type":"object","properties":{"title":{"type":"string","description":"Required; max 255 characters."},"short_description":{"type":"object","nullable":true},"technologies":{"nullable":true,"type":"array","items":{"type":"string"}},"thumbnail":{"type":"object","nullable":true},"video":{"type":"object","nullable":true},"about":{"type":"object","nullable":true},"problem":{"type":"object","nullable":true},"solutions":{"type":"object","nullable":true},"service":{"type":"object","nullable":true,"description":"No max:255 on update (Laravel parity)."}}},"ListProjectsQueryDto":{"type":"object","properties":{"search":{"type":"string"},"technologies":{"type":"string","description":"Single technology value (Laravel `whereJsonContains` parity)."},"service":{"type":"string"},"sort_by":{"type":"string","description":"Allowlisted in service; unknown values fall back to `title`."},"order":{"type":"string","enum":["asc","desc"],"description":"When omitted, listing sorts ascending (Laravel) while `filters.order` in the response defaults to `desc` (R4.7)."},"page":{"type":"number","minimum":1,"default":1}}},"PublicProjectsQueryDto":{"type":"object","properties":{"search":{"type":"string","description":"LIKE match on `service` column (Laravel parity)."},"technologies":{"type":"string"},"service":{"type":"string"}}},"MediaValidationErrorSchema":{"type":"object","properties":{"error":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"example":{"image":["The image field is required."]}}},"required":["error"]},"LaravelServiceSwaggerDto":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"slug":{"type":"string"},"description":{"type":"object","nullable":true},"image":{"nullable":true,"description":"Array of strings when set.","type":"array","items":{"type":"string"}},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","title","slug","description","image","created_at","updated_at"]},"CreateServiceDto":{"type":"object","properties":{"title":{"type":"string"},"slug":{"type":"string","description":"Must be unique across `services.slug`."},"description":{"type":"object","nullable":true},"image":{"nullable":true,"description":"Array of image URL/path strings.","type":"array","items":{"type":"string"}}},"required":["title","slug"]},"UpdateServiceDto":{"type":"object","properties":{"title":{"type":"string"},"slug":{"type":"string","description":"Must be unique across `services.slug`."},"description":{"type":"object","nullable":true},"image":{"nullable":true,"description":"Array of image URL/path strings.","type":"array","items":{"type":"string"}}},"required":["title","slug"]},"ListServicesQueryDto":{"type":"object","properties":{"search":{"type":"string"},"sort_by":{"type":"string","description":"Allowlisted: title, id, slug, created_at, updated_at, description."},"order":{"type":"string","description":"`asc` or `desc` for sorting (default asc). Response `filters.order` defaults to `desc` when omitted (R4.11)."},"page":{"type":"number","minimum":1,"default":1}}},"LaravelBlogSwaggerDto":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"slug":{"type":"string"},"description":{"type":"object","nullable":true},"tags":{"type":"array","items":{"type":"string"}},"image":{"type":"object","nullable":true},"key_takeaways":{"type":"object","nullable":true},"author":{"type":"object","nullable":true},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","title","slug","description","tags","image","key_takeaways","author","created_at","updated_at"]},"CreateBlogDto":{"type":"object","properties":{"title":{"type":"string"},"slug":{"type":"string","description":"Must be unique across `blogs.slug`."},"description":{"type":"object","nullable":true},"tags":{"nullable":true,"description":"Array of tag strings (`tags.*` max 50 chars).","type":"array","items":{"type":"string"}},"image":{"type":"object","nullable":true},"key_takeaways":{"type":"object","nullable":true},"author":{"type":"object","nullable":true}},"required":["title","slug"]},"UpdateBlogDto":{"type":"object","properties":{"title":{"type":"string"},"slug":{"type":"string","description":"Must be unique across `blogs.slug`."},"description":{"type":"object","nullable":true},"tags":{"nullable":true,"description":"Array of tag strings (`tags.*` max 50 chars).","type":"array","items":{"type":"string"}},"image":{"type":"object","nullable":true},"key_takeaways":{"type":"object","nullable":true},"author":{"type":"object","nullable":true}},"required":["title","slug"]},"ListBlogsQueryDto":{"type":"object","properties":{"search":{"type":"string","description":"Substring match on `title` (ILIKE)."},"tags":{"type":"string","description":"Single tag value; blogs whose `tags` JSON array contains this string (PostgreSQL `jsonb @>` parity with Laravel `whereJsonContains`)."},"page":{"type":"number","minimum":1}}},"PublicBlogsQueryDto":{"type":"object","properties":{"search":{"type":"string","description":"LIKE / ILIKE on `title` (R4.25 list mode)."},"page":{"type":"number","minimum":1}}},"LaravelServiceTypeSwaggerDto":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"object","nullable":true,"description":"Nullable in DB after Laravel migration."},"slug":{"type":"string"},"description":{"type":"object","nullable":true},"icons":{"type":"object","nullable":true,"description":"Decoded JSON value (object, array, primitive, or null).","additionalProperties":true},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["id","slug","description","icons","created_at","updated_at"]},"CreateServiceTypeDto":{"type":"object","properties":{"title":{"type":"string"},"slug":{"type":"string","description":"Must be unique across `service_types.slug`."},"description":{"type":"object","nullable":true},"icons":{"type":"object","nullable":true,"description":"Laravel `json` rule: JSON string, or a JSON object/array in the body (see blueprint R4.20)."}},"required":["title","slug"]},"UpdateServiceTypeDto":{"type":"object","properties":{"title":{"type":"object","nullable":true},"slug":{"type":"string"},"description":{"type":"object","nullable":true},"icons":{"type":"object","nullable":true,"description":"Any JSON-serializable value; not strictly validated (R4.21)."}}},"ListServiceTypesQueryDto":{"type":"object","properties":{"search":{"type":"string","description":"Substring match on `title` (ILIKE)."},"page":{"type":"number","minimum":1}}}}}}