{
    "openapi": "3.1.0",
    "info": {
        "title": "Billboard AI",
        "version": "1.0.0",
        "description": "Backend API for **Billboard AI Technology**, a platform connecting\nbrands with influencers for paid and service-based marketing\ncampaigns \u2014 covering campaign discovery and creation, influencer\napplications, content submission and review, wallet-based payments\nvia M-Pesa, and authentication through Google, Apple, and TikTok.\n---\n\n## 1. Core domain structure\n\nThe system is organized as:\n\nA **Brand** belongs to a user and owns campaigns. A **Campaign** is\neither:\n\n| Type | Description |\n|---|---|\n| `paid` | Has a `budget` and a `payment_plan` (`fixed` per creator, or `performance` per view) |\n| `service` | No monetary budget; defined instead by a `service_type` |\n\nInfluencers apply to campaigns (`CampaignApplication`). Once an\napplication is approved, the influencer can submit content\n(`CampaignSubmission`) against the deliverables tied to that\napplication \u2014 post URL, platform, and optional screenshots.\n\n---\n\n## 2. Campaign lifecycle\n\n**Creation & editing**\nCampaigns capture: title, description, products, creator\ndos/don'ts, preferred platforms, target audience (description,\nage range, gender), required content types (`reels`,\n`short_video`, `long_video`, `story`, `static_post`, `carousel`,\n`live`, `podcast`, `blog`), required content styles\n(`storytelling`, `review`, `reaction`, `tutorial`, `dance`,\n`lifestyle_vlog`, `unboxing`, `comedy`, `educational`,\n`interview`, `challenge`, `ugc`), timeline, goals, primary CTA,\nwebsite URL, required hashtags/mentions, deliverables, and a\ncover image (stored on the public disk, replaced on update,\nremoved on delete).\n\n**Status**\nCampaigns move through `draft`, `active`, `paused`, and\n`completed`. Status drives the brand dashboard metrics below and\ndetermines whether normal edit endpoints are available \u2014 an\n`active` campaign that needs its AI embedding or brief\nregenerated uses a dedicated **AI sync** endpoint instead of the\nstandard update flow.\n\n**AI sync**\nTriggering AI sync touches the campaign so a model observer fires,\npushing the latest campaign + brand data to the AI engine for\nembedding generation (used for influencer\u2013campaign matching). If\nthe campaign's brief hasn't yet been approved, sync also\nre-fires the brief-generation webhook.\n\n**Dashboard metrics**\nPer brand: active/draft/paused/completed campaign counts, total\nbudget across active campaigns, and an estimated total reach,\ncalculated per paid campaign as `(budget \u00f7 price_per_creator) \u00d7\n5000` and summed \u2014 a rough proxy for audience size based on\nspend efficiency, not a guaranteed delivery figure.\n\n---\n\n## 3. Applications & submissions\n\n**Applications**\nAn influencer applies to a campaign once; the application can\nlater be withdrawn. Listing applications is permission-scoped \u2014\nbrands see applications to their own campaigns, influencers see\ntheir own applications.\n\n**Deliverables & submissions**\nEach approved application has one or more\n`ApplicationDeliverable` records describing what content is owed.\nInfluencers submit against a specific deliverable with:\n\n| Field | Notes |\n|---|---|\n| `application_deliverable_id` | Must belong to an approved application |\n| `post_url` | Link to the live post |\n| `platform` | Platform the content was posted on |\n| `influencer_screenshots` | Optional proof images, stored on the public disk |\n| `private_metrics_submitted` | Whether private platform analytics were also provided |\n\nAn influencer can only fetch deliverables and submit content for\ncampaigns where they have an **approved** application; otherwise\nthe API returns the reason (e.g. no approved application found)\nrather than the deliverable list.\n\n---\n\n## 4. Payments \u2014 M-Pesa & ledger\n\nWallets are not a separate balance column \u2014 balance is derived\nlive from the **ledger**, by reducing all `credit`, `debit`, and\n`fee` entries for the user.\n\n**Deposits**\n`STK Push` is initiated for a phone number and amount; a\n`pending` credit ledger entry is created keyed on the\n`CheckoutRequestID`. M-Pesa's asynchronous callback then updates\nthat entry to `successful` (replacing the reference with the\nM-Pesa receipt number) or `failed`, based on the callback's\nresult code.\n\n**Withdrawals**\nWithdrawal amount must fall within an **active withdrawal\ntariff** band (`min_amount`\u2013`max_amount`), which determines the\nflat `fee` charged. Before processing, the system:\n\n1. Rejects amounts below the lowest active tariff's minimum.\n2. Rejects duplicate withdrawal attempts (same user, same amount,\n   still `pending`/`processing`, within the last minute).\n3. Locks the user's ledger rows and recomputes current balance;\n   rejects the request if balance is insufficient to cover\n   `amount + fee`.\n4. Creates a `processing` debit entry and a `completed` fee\n   entry, both keyed to a shared UUID reference, inside a single\n   database transaction.\n5. Sends the B2C payout request to M-Pesa.\n\nM-Pesa's B2C result callback then marks the debit `successful` or\n`failed` by matching on `OriginatorConversationID`; a separate\ntimeout callback is logged but does not alter ledger state.\n\n**Transactions**\nAll ledger activity is queryable per user, filterable by `type`,\n`status`, and partial `reference` match, and paginated.\n\n---\n\n## 5. Authentication\n\nThree OAuth paths are supported, all issuing a Sanctum\n`authToken` on success and redirecting to the frontend with the\ntoken in the query string:\n\n| Provider | Notes |\n|---|---|\n| **Google / Apple** | Via Socialite, stateless; matched/created by `email` |\n| **TikTok** | Custom flow (Socialite has no first-party TikTok driver); state parameter is an encrypted, timestamped payload (5-minute expiry) used as stateless CSRF protection instead of session state |\n\n**TikTok specifics**\nScopes requested: `user.info.basic`, `user.info.profile`,\n`user.info.stats`. The callback exchanges the auth code for an\naccess token, fetches the TikTok profile (`open_id`, `username`,\n`avatar_url`, `follower_count`), and matches/creates the user by\n`tiktok_open_id` \u2014 **not** email, since TikTok accounts created\nthis way have no email on file (`email` and `email_verified_at`\nare left `null`).\n\n**Account numbers**\nNew users (via any social provider) are assigned a sequential\naccount number prefixed `BBAI` + two-digit year (e.g.\n`BBAI26000001`), generated by finding the highest existing number\nwith that prefix and incrementing it.\n\n---\n\n## 6. Notifications\n\nPer-user notification feed with unread counts grouped by type,\nand a mark-as-read action restricted to the notification's\nowner.\n\n---\n\n## 7. Roles & access\n\n| Role | Access |\n|---|---|\n| **Brand** | Create/update/delete own campaigns, view own applications and submissions, view own dashboard metrics and budget totals |\n| **Influencer** | Apply to campaigns, withdraw own applications, submit deliverables for approved applications, view own submissions, manage own wallet (deposit/withdraw), view own transactions and notifications |\n\nAuthorization is enforced via Laravel policies at the\ncontroller level (`authorize()` calls) for campaigns and\napplications; ownership checks are enforced directly in the\ncontroller for notifications, submissions, and transactions.\n\n---\n\n## 8. Conventions used across this API\n\n- **Auth**: Bearer token (see the Authorize button in this UI).\n- **Money**: KES, represented as decimal/float in JSON.\n- **File uploads**: cover images and screenshots are stored on\n  the `public` disk; responses return the public `Storage::url()`\n  path.\n- **Webhooks**: M-Pesa STK and B2C callbacks, and the campaign\n  brief webhook, are unauthenticated and idempotent on missing\n  references.\n- **Errors**: standard Laravel validation/error JSON shape \u2014\n  `{ \"message\": \"...\", \"errors\": { \"field\": [\"...\"] } }` for 422,\n  `{ \"message\": \"...\" }` for 401/403/404/500.\n- **Pagination**: list endpoints are paginated (`page`,\n  `per_page` query params), returning Laravel's standard\n  paginator shape.\n- **Dates**: ISO 8601 (`YYYY-MM-DD`).\n\n---\n\nThis API is consumed by the brand dashboard, the influencer app,\nand the AI matching engine."
    },
    "servers": [
        {
            "url": "https://backend.billboardai.tech/api/v1"
        }
    ],
    "tags": [
        {
            "name": "Authentication",
            "description": "OTP-based email authentication for AuthController (v1). A single email + 6-digit-code endpoint pair (`sendOtp` / `verifyOtp`) is reused across four distinct purposes \u2014 `register`, `login`, `payment`, and `set-2fa` \u2014 with the purpose bound into the OTP itself so a code issued for one purpose cannot be redeemed for another. `logout` revokes only the current device's bearer token; other active sessions on other devices are unaffected. Despite the name, this tag does not cover social login \u2014 see \"Social Authentication\" for Google/Apple/TikTok."
        },
        {
            "name": "Social Authentication",
            "description": "OAuth login via Google and Apple (through Socialite, stateless mode, matched by email) and a hand-rolled TikTok flow (custom encrypted state parameter, matched by `tiktok_open_id` since TikTok does not reliably provide an email). Google/Apple accounts can implicitly merge into an existing account sharing the same email; TikTok accounts are created with `email` explicitly null and so never merge with email-based accounts. All three issue a Sanctum bearer token and redirect to the frontend with it in the query string."
        },
        {
            "name": "User Management",
            "description": "Self-service profile management v1\\UserController`) plus the platform-wide admin equivalent (`Api\\UserApiController`). v1's `updateProfile` lets a user manage their own brand or influencer profile and switch between the two roles on any call, with no removal of the previous role's data. The admin side can list, update, suspend, activate, verify, and delete any user \u2014 including granting the `Admin` role itself, the only place in the API that role is assignable. Unlike every other controller in this API, the admin side returns raw `User` models with no Resource wrapping, so any column not in `User::$hidden` is exposed as-is."
        },
        {
            "name": "AI User Management",
            "description": "AI-powered user insights, profile analysis, and recommendations. Provides AI-driven user segmentation and intelligence."
        },
        {
            "name": "Campaign Management",
            "description": "Brand/influencer-facing campaign lifecycle (`v1\\CampaignController`) plus the admin moderation surface for the same data (`Api\\CampaignApiController`). v1 covers discovery, a brand's own management view, creation/update supporting both `paid` and `service` campaign types, deletion, budget /dashboard reporting, and AI embedding resync. The admin side covers cross-brand search, approve/decline/suspend status moderation with no state-machine enforcement between them, and deliverable creation (including a currently byte-identical \"AI\" variant of the same method)."
        },
        {
            "name": "Campaign Applications",
            "description": "Influencer-facing application lifecycle (`v1\\CampaignApplicationController`: apply, view own, withdraw \u2014 a status change, not a deletion) alongside the admin review surface (`Api\\CampaignApplicationApiController`: list by campaign or platform-wide, accept, reject with a required reason). Acceptance/rejection delegate their actual state-transition rules entirely to `CampaignApplicationService`."
        },
        {
            "name": "Campaign Submissions",
            "description": "The full post-approval content lifecycle, spanning five controllers. Influencer side (`v1\\CampaignSubmissionController`): fetch deliverables for an approved application, submit proof of posted content, view/delete own submissions. Admin review (`Api\\CampaignSubmissionController`): cross-influencer/ campaign submission visibility, and approve/reject actions that cascade into deliverable status, application completion percentage, and notifications \u2014 neither guarded against being re-run on an already-decided submission, and the approval cascade is not wrapped in a transaction. Post-approval monitoring (`Api\\SubmissionCheckReminderController`, `Api\\SubmissionProgressionController`): scheduling and recording follow-up engagement checks on live content \u2014 the progression check-numbering has its own race condition on concurrent writes, and both controllers carry an injected, currently-unused `NotificationService`."
        },
        {
            "name": "Location Management",
            "description": "Read-only public lookups (`v1\\LocationController`: countries with full recursive subtree, a single location's children by ID, free-text search across all types) plus admin CRUD (`Api\\LocationApiController`) for managing the hierarchy itself. Admin `update()`'s children handling is a destructive replace, not a merge \u2014 any existing child not included in the request is deleted; admin `destroy()`'s own docblock claims cascading child deletion that the visible code does not itself perform."
        },
        {
            "name": "M-Pesa Payments",
            "description": "Wallet funding and payout via Safaricom's M-Pesa APIs, backed entirely by `Ledger` rows rather than a stored balance column. `deposit` initiates an STK Push and writes a pending credit row keyed on `CheckoutRequestID`; `callback` resolves it to successful or failed \u2014 but does not persist M-Pesa's confirmed amount if it differs from the originally requested one, and overwrites the lookup key after first success in a way that would miss a resent duplicate callback. `withdraw` validates against active tariff bands, locks and recomputes the user's ledger balance, reserves the withdrawal and its fee inside a database transaction, then calls out to M-Pesa's B2C API outside that transaction; `b2cResult` and `b2cTimeout` resolve \u2014 or, on timeout, fail to resolve \u2014 that payout's final status, with the paired fee entry never reversed on a failed withdrawal."
        },
        {
            "name": "Transactions",
            "description": "Read-only access to the same `Ledger` rows written by \"M-Pesa Payments\": a paginated, filterable history scoped to the authenticated user (`index`, filterable by `type`, `status`, and partial `reference` match \u2014 useful for matching a withdrawal's paired fee row via its `-FEE` reference suffix), and a single-transaction lookup by ID (`show`) \u2014 the latter currently has no ownership scoping visible in the controller, worth verifying against route middleware before treating it as confirmed-safe."
        },
        {
            "name": "Notifications",
            "description": "The authenticated user's notification feed in full (`index`, unpaginated), a lighter unread-count summary grouped by type for badges and nav indicators (`counts`), and marking a single notification read (`markAsRead`) \u2014 a status change rather than a deletion, so read notifications remain visible in `index` but drop out of `counts`. The only controller in the API enforcing ownership via a plain in-controller equality check rather than a policy or service-side scope."
        },
        {
            "name": "AI Support Agent",
            "description": "AI-powered support and assistance through real-time chat. Provides context-aware responses and streaming conversation."
        },
        {
            "name": "Recruitment",
            "description": "Public job application intake for Billboard AI's own hiring, unrelated to brand/influencer campaign workflows. `getRoles` lists currently open roles to populate an application form's role selector \u2014 not enforced against what `store` actually accepts; `store` takes one shared application form covering several distinct role categories (product management, engineering, advisory) at once, with every role-specific field optional rather than conditionally required by the chosen role, plus a resume upload and an optional portfolio (files and/or a URL)."
        },
        {
            "name": "TikTok Authentication",
            "description": "A hand-rolled TikTok OAuth flow, kept separate from \"Social Authentication\" because TikTok has no first-party Socialite driver. `redirect` builds and encrypts its own CSRF state parameter (a nonce plus timestamp); `callback` validates that state's 5-minute expiry, exchanges the authorization code for an access token, fetches the TikTok profile, and matches or creates a user by `tiktok_open_id` rather than email \u2014 TikTok accounts are created with `email` and `email_verified_at` explicitly null, since TikTok does not reliably supply an email address at all."
        },
        {
            "name": "Campaign Brief Management (Admin)",
            "description": "Admin review of AI-generated campaign briefs: a queue of campaigns awaiting review (`pendingBriefs`), inspecting a specific draft (`getBrief`), editing it before approval (`updateBrief`), approving it (`approveBrief`), and forcing Django to regenerate it (`regenerateBrief`). Brief approval is a separate action from campaign approval \u2014 a campaign can be `active` with an unapproved brief, or vice versa. Note: in the normal flow, `AI\\AiSyncController::campaignBriefCallback()` appears to set `brief_status` directly to `approved` the moment Django responds, bypassing the `pending_review` state this admin workflow is built around \u2014 worth resolving whether this admin review step is currently reachable in production before relying on its existence."
        },
        {
            "name": "AI Campaign Matching",
            "description": "[PLACEHOLDER NAME \u2014 pending rename] Influencer-facing campaign recommendations (`AI\\CampaignMatchingController::recommendations()`): calls Django synchronously (8s timeout) for ranked campaign IDs, fetches the matching active `Campaign` rows, and re-sorts by score after the fact since `whereIn()` does not preserve input order. The only AI-calling endpoint in the API that fails open \u2014 any failure mode (bad response, empty results, timeout, exception) returns an identical \"no recommendations\" success response, making a real AI-service outage indistinguishable from \"no matches\" at the API contract level."
        },
        {
            "name": "AI IAP Management",
            "description": "[PLACEHOLDER NAME \u2014 pending rename] Thin proxy to Django's influencer AI profile (\"IAP\") service (`AI\\IapController`) \u2014 `show` fetches a profile live with no local caching; `regenerate` forces a rebuild synchronously (30s timeout) and returns `201`. Fails closed on every error path with distinct status codes (404/503/500), in contrast to AI Campaign Matching's fail-open behavior. Structurally identical to AI ICP Management, mirrored for influencers instead of brands."
        },
        {
            "name": "AI ICP Management",
            "description": "[PLACEHOLDER NAME \u2014 pending rename] The brand-side mirror of AI IAP Management (`AI\\IcpController`) \u2014 same `show`/ `regenerate` shape, same fail-closed error handling, targeting a brand's \"Ideal Customer Profile\" instead of an influencer profile. Structurally identical to its influencer counterpart in every respect that matters."
        },
        {
            "name": "AI Influencer Matching",
            "description": "[PLACEHOLDER NAME \u2014 pending rename] Brand-facing influencer search (`AI\\MatchingController`) \u2014 the reciprocal half of AI Campaign Matching: `search` asks Django which influencers match the brand's campaign (30s timeout); `results` retrieves Django's cached search output (10s timeout). Fails closed like AI IAP/ICP Management, but its exception handler is the only one in the AI namespace that returns the raw exception message in the response body rather than a generic error string \u2014 worth fixing as a precaution against leaking internal details. `campaign_id` here is validated as a UUID, unlike every other campaign-ID reference elsewhere in the API \u2014 worth confirming which typing is actually correct."
        }
    ],
    "security": [
        {
            "http": []
        }
    ],
    "paths": {
        "/user/account-balance": {
            "get": {
                "tags": [
                    ""
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "balance": {
                                            "type": "string"
                                        }
                                    },
                                    "required": [
                                        "balance"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/webhooks/tiktok": {
            "post": {
                "tags": [
                    ""
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "status": {
                                            "type": "string",
                                            "const": "ok"
                                        }
                                    },
                                    "required": [
                                        "status"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/ai/matching/recommendations": {
            "get": {
                "operationId": "campaignMatching.recommendations",
                "description": "The only place in this entire API where Django's AI service is\ncalled synchronously, in the request/response path of an\ninfluencer-facing endpoint, rather than as a fire-and-forget\nwebhook (contrast with `syncAi()`/`regenerateBrief()`'s brief\ngeneration, which is async \u2014 fire the webhook, return\nimmediately, let a callback resolve it later). That makes this\nendpoint's latency directly dependent on Django's response time,\nbounded by an 8-second timeout.\n\n**Fails open, not closed, on every AI-service problem.** A\nfailed HTTP response, an empty `recommendations` array, *and*\nany thrown exception (timeout, connection refused, malformed\nJSON) all return the same `success` response with an empty\n`campaigns` array and a 200-equivalent status \u2014 there is no\nerror status surfaced to the frontend for \"the AI service is\n     * down\" versus \"you genuinely have zero matching campaigns right\n     * now.\" An influencer with a broken recommendations feed and an\ninfluencer who's matched to nothing would see an identical\nempty state, and nothing in the response distinguishes them.\nWhether that's the right product decision (probably yes \u2014 a\nbroken recommendations widget degrading to \"no recommendations\"\nrather than a visible error is generally the friendlier\nfailure mode) is a judgment call, but it does mean this\nspecific failure mode is invisible to anyone monitoring via the\nAPI responses alone \u2014 only the `Log::warning()` calls would\nreveal it, and those are warnings, not errors, despite\nrepresenting a complete AI-service outage from this endpoint's\nperspective.\n\n`platform` is read from the query string and forwarded to\nDjango unvalidated \u2014 no `in:` enum check the way\n`v1\\CampaignController`'s `required_content_types` validates\nplatform-adjacent fields; whatever Django does with an\nunrecognized platform value is opaque from this side.\n`limit` defaults to 20 with no upper bound enforced here either\n\u2014 a caller requesting `limit=10000` passes that straight through\nto Django, whose own behavior at that point is unknown from this\nfile.\n\n**The result is not actually sorted in score order by the time\nit's returned, despite the docblock's claim.** Walking through\nthe logic: `$scoreMap` is built from Django's response, which\nthe docblock describes as already ranked; campaigns are fetched\nvia `whereIn('id', $campaignIds)`, and Eloquent's `whereIn` does\n**not** guarantee result order matches the order of the IDs\npassed in \u2014 most databases return `whereIn` results in\nwhatever order the underlying index/storage happens to produce,\nnot the array's order. The subsequent `->map()` preserves\nwhatever order `get()` returned, attaching the correct\n`match_score` to each campaign \u2014 so the *scores* are correct,\nbut the **explicit `->sortByDesc('match_score')` immediately\nafter the map** is what actually fixes the ordering, by\nre-sorting after the fact rather than relying on `whereIn` to\nhave preserved it. The code's own self-correction makes this a\nnon-issue in the final output \u2014 `sortByDesc` does land it in\nthe right order \u2014 but the inline comment (\"Fetch campaigns in\n     * one query, preserving score order\") describes the `whereIn`\ncall as the thing doing the preserving, when it's actually the\n`sortByDesc` two steps later doing the real work; worth fixing\nthe comment so a future reader doesn't remove `sortByDesc` as\n\"redundant\" based on trusting that comment.\n\n**A campaign Django recommends that is no longer `active`\n(paused, completed, deleted) is silently dropped** by the\n`where('status', 'active')` filter \u2014 Django's `$ranked` list and\nthe final `$campaigns` count can differ, with no indication in\nthe response of how many recommendations were filtered out this\nway, which could make the response feel oddly short relative to\nthe `limit` requested if several of Django's top picks happen\nto be stale by the time this endpoint is called.\n\n`cover_image` in the response is read from\n`$campaign->cover_image_url` \u2014 note the key rename\n(`cover_image_url` on the model becomes `cover_image` in this\nresponse), which differs from how `CampaignResource` presumably\nnames this field elsewhere in the API; a frontend consuming both\nthis endpoint and a `CampaignResource`-wrapped one needs to\nhandle two different key names for what's likely the same\nunderlying image.",
                "summary": "Recommendations",
                "tags": [
                    "AI Campaign Matching"
                ],
                "parameters": [
                    {
                        "name": "platform",
                        "in": "query",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Influencer profile not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/ai/iap/{influencerId}": {
            "get": {
                "operationId": "iap.show",
                "description": "GET equivalent of fetching one influencer's IAP profile directly\nfrom Django, with a 10-second timeout. Three failure paths are\neach given a distinct, meaningful status: Django's own `404`\n(no IAP profile generated yet for this influencer) is passed\nstraight through as this endpoint's `404`; any other non-2xx\nresponse logs at `error` level and returns `503` (\"AI service\n     * unavailable\" \u2014 i.e. reached, but something's wrong on Django's\nside); and a thrown exception (timeout, connection failure)\nlogs separately and returns `500`. This three-way split is more\ngranular than `Api\\CampaignBriefController::regenerateBrief()`'s\ntwo-way split (`502` for a bad response, `503` for a connection\nfailure) \u2014 worth noting since these two controllers use\ndifferent status codes for what's conceptually the same\n\"reached the AI service, but it errored\" case (`503` here vs.\n`502` there), which is a minor inconsistency across the\ncodebase's AI-integration error conventions.\n\nThe successful response is passed through with `$response->json()`\ndirectly as the data payload \u2014 no Resource wrapping, no\nreshaping, unlike `CampaignMatchingController::recommendations()`'s\nmanual field renaming (`cover_image_url` \u2192 `cover_image`). This\nendpoint's response shape is entirely whatever Django's API\nreturns, verbatim \u2014 meaning if Django's IAP response shape\nchanges, this endpoint's contract changes with it with no\nintermediary layer in Laravel to absorb or normalize that.",
                "summary": "Fetch a single influencer's IAP profile from Django",
                "tags": [
                    "AI IAP Management"
                ],
                "parameters": [
                    {
                        "name": "influencerId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "500": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "AI service error"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "AI service unavailable"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "IAP profile not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/ai/iap/regenerate": {
            "post": {
                "operationId": "iap.regenerate",
                "description": "Forces Django to regenerate an influencer's IAP profile from\nscratch, optionally seeded with extra `data` the caller supplies\n\u2014 the IAP equivalent of\n`Api\\CampaignBriefController::regenerateBrief()`, but for\ninfluencer profiles instead of campaign briefs, and called\ndirectly rather than via a fire-and-forget webhook pattern:\nthis method waits synchronously (30-second timeout, the longest\nin this codebase, reflecting that regeneration is presumably\nmore expensive than a simple fetch) for Django's response and\nreturns it inline, rather than returning immediately and\nletting a separate callback (like\n`AI\\AiSyncController::influencerDemographicsCallback()`) resolve\nit later. That's a meaningfully different integration pattern\nfor what's conceptually a similar \"ask Django to redo AI work\"\naction elsewhere in the codebase \u2014 worth knowing if you're\ntrying to standardize how these AI-triggering endpoints behave,\nsince right now there are at least two different patterns in\nuse (async webhook+callback vs. synchronous wait) for what's\nfunctionally the same kind of request.\n\n`influencer_id` is validated as `required|integer` here \u2014 unlike\n`AI\\AiSyncController::influencerDemographicsCallback()`, which\nreads `influencer_id` from the request with only a manual\ntruthiness check and no `validate()` call at all. This method\ndoes not verify the influencer actually exists in Laravel's own\ndatabase before forwarding the ID to Django \u2014 an `influencer_id`\nfor a deleted or never-existing influencer would pass this\nmethod's validation and be Django's problem to reject, not\nLaravel's.\n\nReturns a `201` on success \u2014 the only `201` response in any AI\ncontroller reviewed so far (`AiSyncController` and\n`AIUserController` both return `200`-equivalent success for\nwrites); using `201` here implies \"a new resource was created,\"\nwhich is arguably accurate for a *regeneration* in the sense of\nproducing a new profile version, but is a different semantic\nchoice than `AI\\AIUserController::upsertAIProfile()` makes for\nits own create-or-update action, which doesn't distinguish `201`\nfrom `200` despite also potentially creating a new row via\n`updateOrCreate()`.",
                "summary": "Regenerate an influencer's IAP profile from Django",
                "tags": [
                    "AI IAP Management"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "influencer_id": {
                                        "type": "integer"
                                    },
                                    "data": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    }
                                },
                                "required": [
                                    "influencer_id"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "500": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "AI service error"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "201": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "AI service unavailable"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/ai/icp/{brandId}": {
            "get": {
                "operationId": "icp.show",
                "description": "Fetches one brand's ICP directly from Django, with the\nsame three-status failure handling as `IapController::show()`\n(`404` for \"Django has no ICP for this brand yet,\" `503` for\nany other failed response, `500` for a thrown exception/timeout\nat 10 seconds). Response is passed through verbatim via\n`$response->json()`, with no Resource wrapping and no\nreshaping \u2014 same as `IapController::show()`, this endpoint's\ncontract is whatever Django's ICP response shape happens to be.\n\nNo check that `$brandId` corresponds to an actual `Brand` row\nin Laravel's own database before forwarding it to Django \u2014\nsame gap as `IapController::show()`'s influencer equivalent.",
                "summary": "Fetche Brand's ICP",
                "tags": [
                    "AI ICP Management"
                ],
                "parameters": [
                    {
                        "name": "brandId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "500": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "AI service error"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "AI service unavailable"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "ICP profile not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/ai/icp/regenerate": {
            "post": {
                "operationId": "icp.regenerate",
                "description": "Forces Django to regenerate a brand's ICP profile, identical in\nstructure to `IapController::regenerate()`: synchronous\n(30-second timeout), validates `brand_id` as `required|integer`\nwith no existence check against Laravel's own `Brand` table,\naccepts an optional `data` array forwarded as-is to Django, and\nreturns `201` on success \u2014 same semantic choice (and same\ninconsistency with `AI\\AIUserController::upsertAIProfile()`'s\nundifferentiated `200`) flagged in `IapController::regenerate()`.\n\nWorth a direct side-by-side check of this pair\n(`IapController`/`IcpController`) against\n`Api\\CampaignBriefController::regenerateBrief()`'s `502`/`503`\nsplit for \"AI service reached but errored\" vs. \"AI service\n     * unreachable\" \u2014 these two controllers use `503` for both cases\n(the bad-response case and the connection-failure case both\nreturn `503`, distinguished only by which `catch`/`if` branch\nproduced them, with identical status codes), while\n`regenerateBrief()` uses `502` specifically for the\nbad-response case. Three different files, three slightly\ndifferent status-code conventions for what's conceptually the\nsame two failure modes \u2014 worth picking one standard if this\never gets audited for API consistency.",
                "summary": "Force Regenerate Brand's ICP",
                "tags": [
                    "AI ICP Management"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "brand_id": {
                                        "type": "integer"
                                    },
                                    "data": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    }
                                },
                                "required": [
                                    "brand_id"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "500": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "AI service error"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "201": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "AI service unavailable"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/ai/matching/search": {
            "post": {
                "operationId": "matching.search",
                "description": "Triggers a live matching search against Django, scoped to the\nauthenticated user's own `brand` (derived from `auth()`/`request()->user()`,\nnever from a request parameter \u2014 a brand cannot search on\nbehalf of another brand by ID). `campaign_id` is optional: when\nomitted, this presumably asks Django for influencer matches\nacross the *entire* brand rather than one specific campaign,\nthough nothing in this method's code confirms what Django does\nwith a null `campaign_id` \u2014 that behavior lives entirely on the\nDjango side. When provided, it's validated as a UUID, which is\nnotable: every other `campaign_id`/`id` reference across this\nwhole API (`v1\\CampaignController`, `Api\\CampaignApiController`,\netc.) treats campaign IDs as plain integers or unconstrained\nstrings \u2014 this is the only validation rule in the entire\ncodebase reviewed so far that explicitly expects campaigns to\nbe UUID-keyed. Worth confirming whether `Campaign`'s primary key\nactually is a UUID (in which case every other untyped\n`string $id`/`int $id` campaign parameter elsewhere in this API\nhas just been imprecisely typed) or whether this validation\nrule is itself wrong and should be a plain string/integer check\ninstead.\n\n30-second timeout \u2014 same as the `regenerate()` methods in\n`IapController`/`IcpController`, the longest timeout tier used\nfor AI calls in this codebase, here applied to a *read* (search)\nrather than a regeneration, implying influencer-matching search\nis itself an expensive Django-side operation, not just brief/IAP/ICP\nregeneration.\n\nThe exception-path response includes the raw exception message\nin the response body (`['error' => $e->getMessage()]`) \u2014 unlike\nevery other AI controller's `catch` block in this namespace\n(`CampaignMatchingController`, `IapController`, `IcpController`),\nwhich all log the exception message but return only a generic\n\"AI service error\" string to the caller with an empty `[]` data\narray. Returning the raw exception message to the API consumer\nhere is a potential information-disclosure surface \u2014 depending\non what kind of exception is thrown (e.g. a connection exception\nembedding an internal hostname or IP in its message), this could\nleak infrastructure details to whatever's calling this endpoint\nthat every sibling AI controller deliberately withholds.",
                "summary": "Search for matching influencers for a brand's campaign",
                "tags": [
                    "AI Influencer Matching"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "campaign_id": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "uuid"
                                    },
                                    "limit": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 1,
                                        "maximum": 100
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "500": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Matching service error"
                                        },
                                        "errors": {
                                            "type": "object",
                                            "properties": {
                                                "error": {
                                                    "type": "string"
                                                }
                                            },
                                            "required": [
                                                "error"
                                            ]
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Matching service unavailable"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Brand profile not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/ai/matching/{brandId}/results": {
            "get": {
                "operationId": "matching.results",
                "description": "The read path for whatever `search()` triggers \u2014 \"cached\"\nimplies Django stores the search results from a prior `search()`\ncall and this method retrieves them without re-running the\n(expensive, 30-second-timeout-worthy) matching computation. At\n10 seconds, this method's timeout matches `IapController`/`IcpController`'s\n`show()` timeout tier rather than their `regenerate()` tier,\nconsistent with this being a cheap lookup rather than a\ncomputation.\n\nSame brand-scoping as `search()` (derived from the authenticated\nuser, not a request parameter) and the same raw-exception-message\ndisclosure in the `catch` block noted above.\n\nThere's no parameter here for which `campaign_id`'s cached\nresults to fetch \u2014 this hits `/api/ai/matching/{$brand->id}/results/`,\nkeyed purely on the brand. If a brand can run `search()` against\nmultiple different campaigns (since `campaign_id` is optional\nand per-campaign in that method), it's not clear from this\nmethod's signature whether `results()` returns the most recent\nsearch's results regardless of which campaign it was for, all\ncached results across every campaign, or something else \u2014\nthat disambiguation isn't visible from this side of the\nintegration.",
                "summary": "Get cached matching results for a brand",
                "tags": [
                    "AI Influencer Matching"
                ],
                "parameters": [
                    {
                        "name": "brandId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "500": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Matching service error"
                                        },
                                        "errors": {
                                            "type": "object",
                                            "properties": {
                                                "error": {
                                                    "type": "string"
                                                }
                                            },
                                            "required": [
                                                "error"
                                            ]
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "503": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Matching service unavailable"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Brand profile not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/billboard-ai/chat-support": {
            "post": {
                "operationId": "billboardAISupport.chat",
                "description": "Streams AI responses for user support and assistance.\nHandles real-time conversation with context-aware responses.",
                "summary": "Chat with AI support agent",
                "tags": [
                    "AI Support Agent"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "message": {
                                        "type": "string",
                                        "maxLength": 5000
                                    }
                                },
                                "required": [
                                    "message"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "text/event-stream": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        },
                        "headers": {
                            "Transfer-Encoding": {
                                "required": true,
                                "schema": {
                                    "type": "string",
                                    "enum": [
                                        "chunked"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/billboardai-agent/users/{id}": {
            "get": {
                "operationId": "aIUser.show",
                "description": "A single user with `role`, `brand`, `influencer`, and\n`aiProfile` all eagerly loaded and passed through\n`AIUserResource` \u2014 the one controller in the `AI` namespace\nreviewed so far that *does* wrap its output in a Resource class,\nin contrast to `AiSyncController`'s raw `toArray()` dumps and\n`Api\\UserApiController`'s raw model returns. Whether\n`AIUserResource` actually excludes anything sensitive from the\nunderlying `User`/`AIProfile` data is worth checking directly,\nsince a Resource class only protects what it's explicitly\nwritten to protect \u2014 but at minimum the *pattern* here (Resource\nbetween model and response) is the one the rest of this API\nuses correctly, and this method follows it.\n\nIf the user exists but has never had an `AIProfile` created\n(i.e. `upsertAIProfile()` below has never been called for them),\n`aiProfile` loads as `null` via `with()` \u2014 this method has no\nspecial handling for that case beyond whatever\n`AIUserResource` does when the relation is empty; there's no\nseparate \"no AI profile yet\" message the way\n`v1\\UserController::syncAi()` distinguishes \"no profile to\n     * sync\" with its own 422.",
                "summary": "Get AI user profile",
                "tags": [
                    "AI User Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "User not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboardai-agent/users/{id}/ai-profile": {
            "post": {
                "operationId": "ai.aIUser.upsertAIProfile_0",
                "description": "The write path for whatever's read back by `show()` above \u2014\nalmost certainly called by Django itself after running\nsegmentation/scoring on a user (the AI-generated nature of\n`ai_segment`/`ai_maturity_level`/`ai_recommended_action` makes a\nhuman admin manually typing these values into a form unlikely,\nthough nothing in this method's validation rules out a human\ncaller doing exactly that \u2014 there's no distinction enforced\nbetween \"Django wrote this\" and \"an admin overrode it\").\n\n`updateOrCreate()` keyed on `user_id` means this is safe to call\nrepeatedly for the same user \u2014 each call fully replaces the\nprevious AI profile's fields with whatever's provided this time\n(not a merge of old and new), refreshing `ai_last_updated` to\n`now()` on every call regardless of whether anything actually\nchanged.\n\nEvery field is `nullable`, including `ai_segment` and\n`ai_maturity_level` \u2014 a call providing only `ai_features` (or\neven an entirely empty body, since nothing here requires at\nleast one field) would still succeed and still update\n`ai_last_updated`, potentially overwriting a previously-set\n`ai_segment` with `null` if the caller's payload omits a field\nthat already had a value \u2014 `updateOrCreate()`'s second argument\nis exactly what gets written, with `array_merge($validated,\n[...])` only adding the timestamp, not preserving any existing\n`AIProfile` values for fields not present in this request. A\npartial update from Django that only intends to refresh, say,\n`profile_completeness_score` would silently null out\n`ai_segment`/`ai_maturity_level`/`ai_recommended_action`/`ai_features`\nif those keys are simply absent from the validated payload\nrather than explicitly set to their current values \u2014 worth\nconfirming this is how Django's caller actually sends these\nrequests (always all fields, never a true partial update)\nbefore this surprises someone.\n\n`ai_features` accepts any array shape with no structure\nvalidation beyond \"must be an array\" \u2014 unlike\n`v1\\CampaignController`'s `required_content_types`/`required_content_styles`,\nwhich constrain array contents to a fixed enum, there's nothing\nhere constraining what `ai_features` can contain, so its shape\nis effectively whatever Django happens to send, undocumented at\nthe validation layer.",
                "summary": "Create or update AI profile",
                "tags": [
                    "AI User Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "ai_segment": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 100
                                    },
                                    "ai_maturity_level": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 100
                                    },
                                    "profile_completeness_score": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0,
                                        "maximum": 100
                                    },
                                    "ai_recommended_action": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 2000
                                    },
                                    "ai_features": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "User not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            },
            "patch": {
                "operationId": "ai.aIUser.upsertAIProfile_1",
                "description": "The write path for whatever's read back by `show()` above \u2014\nalmost certainly called by Django itself after running\nsegmentation/scoring on a user (the AI-generated nature of\n`ai_segment`/`ai_maturity_level`/`ai_recommended_action` makes a\nhuman admin manually typing these values into a form unlikely,\nthough nothing in this method's validation rules out a human\ncaller doing exactly that \u2014 there's no distinction enforced\nbetween \"Django wrote this\" and \"an admin overrode it\").\n\n`updateOrCreate()` keyed on `user_id` means this is safe to call\nrepeatedly for the same user \u2014 each call fully replaces the\nprevious AI profile's fields with whatever's provided this time\n(not a merge of old and new), refreshing `ai_last_updated` to\n`now()` on every call regardless of whether anything actually\nchanged.\n\nEvery field is `nullable`, including `ai_segment` and\n`ai_maturity_level` \u2014 a call providing only `ai_features` (or\neven an entirely empty body, since nothing here requires at\nleast one field) would still succeed and still update\n`ai_last_updated`, potentially overwriting a previously-set\n`ai_segment` with `null` if the caller's payload omits a field\nthat already had a value \u2014 `updateOrCreate()`'s second argument\nis exactly what gets written, with `array_merge($validated,\n[...])` only adding the timestamp, not preserving any existing\n`AIProfile` values for fields not present in this request. A\npartial update from Django that only intends to refresh, say,\n`profile_completeness_score` would silently null out\n`ai_segment`/`ai_maturity_level`/`ai_recommended_action`/`ai_features`\nif those keys are simply absent from the validated payload\nrather than explicitly set to their current values \u2014 worth\nconfirming this is how Django's caller actually sends these\nrequests (always all fields, never a true partial update)\nbefore this surprises someone.\n\n`ai_features` accepts any array shape with no structure\nvalidation beyond \"must be an array\" \u2014 unlike\n`v1\\CampaignController`'s `required_content_types`/`required_content_styles`,\nwhich constrain array contents to a fixed enum, there's nothing\nhere constraining what `ai_features` can contain, so its shape\nis effectively whatever Django happens to send, undocumented at\nthe validation layer.",
                "summary": "Create or update AI profile",
                "tags": [
                    "AI User Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "ai_segment": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 100
                                    },
                                    "ai_maturity_level": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 100
                                    },
                                    "profile_completeness_score": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0,
                                        "maximum": 100
                                    },
                                    "ai_recommended_action": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 2000
                                    },
                                    "ai_features": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "User not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/ai/sync/brands": {
            "get": {
                "operationId": "aiSync.brands",
                "description": "Returns every `Brand` row in full, with no pagination and no\nfield filtering \u2014 `Brand::all()` includes every column the model\nhas, mapped through `toArray()` directly rather than a Resource\nclass, the same raw-model-exposure pattern flagged as a concern\nin `Api\\UserApiController`. The difference here is the audience:\nthis is a machine-to-machine endpoint behind API-key middleware\nfor Django's own backfill job, not a human-facing admin or v1\nresponse \u2014 so the bar for \"should this be wrapped in a\n     * Resource\" is lower, but it's still worth confirming `Brand` has\nno column that shouldn't leave the platform even to an internal\nservice (e.g. anything tied to payment details, if `Brand`\nstores any).",
                "summary": "Brands",
                "tags": [
                    "AiSync"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/ai/sync/influencers": {
            "get": {
                "operationId": "aiSync.influencers",
                "description": "Same shape as `brands()` \u2014 every `Influencer` row, unfiltered,\nunpaginated \u2014 supplying the content taxonomy fields\n(`content_types`, `content_styles`, `content_niche`) and\nengagement metrics that `v1\\UserController::updateProfile()`\nlets an influencer set on their own profile, here read back out\nin bulk for Django's matching-embedding generation.",
                "summary": "Influencers",
                "tags": [
                    "AiSync"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/ai/sync/campaigns": {
            "get": {
                "operationId": "aiSync.campaigns",
                "description": "Unlike `brands()`/`influencers()`, this is filtered to\n`status = 'active'` campaigns only \u2014 a deliberate scope\ndecision for embedding generation (presumably no value in\ngenerating match embeddings for `draft` or `completed`\ncampaigns). Each campaign's brand data is merged into the same\nflat array under `brand_data` rather than left nested under the\n`brand` relation key that `toArray()` would normally produce \u2014\na manual reshape specifically for whatever payload format\nDjango's `CampaignEmbeddingService` expects, distinct from how\n`brand` appears in `CampaignResource`-wrapped responses\nelsewhere in the API.",
                "summary": "Campaigns",
                "tags": [
                    "AiSync"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/ai/callbacks/influencer-demographics": {
            "post": {
                "operationId": "aiSync.influencerDemographicsCallback",
                "description": "The return path for the demographics extraction\n`v1\\UserController::updateProfile()` sets in motion when an\ninfluencer uploads age/gender/location screenshots \u2014 Django\nprocesses those images and calls back here with whatever it\nextracted. Each of the three demographic fields is updated\n**independently and only if non-empty** \u2014 a callback that only\nmanaged to extract `audience_gender` (e.g. the age screenshot\nwas blurry) updates just `gender_demographics`, leaving\n`age_demographics`/`location_demographics` exactly as they were,\nrather than overwriting them with empty values. If all three\nextractions came back empty, the method returns success with an\nexplicit \"no demographics extracted\" message rather than\nsilently doing nothing \u2014 a useful distinction for whoever\nmonitors these callbacks, since it's a normal outcome (bad\nscreenshot) rather than an error.\n\n`influencer_id` is read directly from the request body with no\nvalidation rule (`required|exists:influencers,id` or similar) \u2014\njust a manual truthiness check \u2014 and an invalid-but-truthy ID\n(e.g. a non-numeric string) would fail at the `Influencer::find()`\ncall with a clean 404 rather than the empty-check catching it\nearlier, so the end result is correct, just reached via a\ndifferent path than the validation layer used everywhere else\nin this API.\n\nUses `updateQuietly()` specifically to prevent\n`InfluencerObserver` from firing \u2014 since that observer's whole\njob (per earlier controllers) is pushing the influencer's\nprofile back to the AI engine for embedding generation, calling\na *normal* `update()` here would mean every demographics\ncallback re-triggers the exact AI sync that produced the\ncallback in the first place, an infinite loop this method\nexists specifically to avoid.",
                "summary": "Influencer Demographics",
                "tags": [
                    "AiSync"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "influencer_id": {
                                        "type": "string"
                                    },
                                    "audience_age_range": {
                                        "type": "string"
                                    },
                                    "audience_gender": {
                                        "type": "string"
                                    },
                                    "audience_locations": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "anyOf": [
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "Influencer not found"
                                                },
                                                "errors": {
                                                    "type": "integer",
                                                    "const": 404
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "influencer_id is required"
                                                },
                                                "errors": {
                                                    "type": "integer",
                                                    "const": 422
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        }
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/ai/callbacks/campaign-brief": {
            "post": {
                "operationId": "aiSync.campaignBriefCallback",
                "description": "The return path for the brief-generation webhook fired by both\n`v1\\CampaignController::syncAi()` and\n`Api\\CampaignBriefController::regenerateBrief()` \u2014 Django\ngenerates the brief text and calls back here with the result.\n**This is the method that actually sets `brief_status` to\n`'approved'`** \u2014 see the class-level note above for why that\ndirectly conflicts with `Api\\CampaignBriefController`'s\npending-review workflow, which expects an intermediate\n`pending_review` state that this callback skips entirely.\n\nUnconditionally overwrites any existing `ai_brief_draft` \u2014\nthe `$wasExisting` check exists purely for logging (\"overwriting\n     * existing brief\" vs. \"first time\") and has no effect on whether\nthe overwrite happens; there is no protection here against a\nregeneration callback clobbering an admin's manual edit from\n`Api\\CampaignBriefController::updateBrief()` if that edit\nhappened after the regeneration request was sent but before\nDjango's response arrived \u2014 a race between human edit and AI\nregeneration that this method has no way to detect or prevent.\n\nLogs extensively at every step (received, about-to-save,\npost-save verification reading the value back from `$campaign`)\n\u2014 by far the most heavily logged method across every controller\nreviewed in this codebase, which makes sense for a\nfire-and-forget webhook endpoint where the calling service\n(Django) has no way to inspect this side's database state\ndirectly; the logs are effectively the only debugging surface\navailable for this integration point.\n\nValidates `campaign_id` and `brief_text` manually (truthiness\nand `is_string` checks) rather than via `$request->validate()` \u2014\nconsistent with `influencerDemographicsCallback()` above, this\nis the pattern across both callback methods in this file,\ndistinct from the `validate()`-based approach used everywhere\nelse in this API.\n\n**Possible bug, worth verifying directly:** every\n`ApiHelper::error()` call in this method passes the HTTP status\ncode as the call's **second** argument\n(`ApiHelper::error('message', [], 422)` is the pattern used\neverywhere else in this codebase; here it's\n`ApiHelper::error('campaign_id is required', [], 422)` \u2014 wait,\nchecking the actual code: this method's calls are\n`ApiHelper::error('campaign_id is required', [], 422)` for the\nvalidate calls... Let me flag precisely: comparing this\nmethod's `ApiHelper::error('campaign_id is required', [], 422)`\nstyle calls against `influencerDemographicsCallback()`'s\n`ApiHelper::error('influencer_id is required', 422)` \u2014 the\nlatter omits the empty `[]` data array entirely and passes\n`422` as the **second** positional argument instead of the\nthird. If `ApiHelper::error()`'s real signature is\n`error(string $message, array $data = [], int $code = 500)`\n(the pattern used in every other controller in this codebase),\nthen `influencerDemographicsCallback()`'s two error calls \u2014\n`ApiHelper::error('influencer_id is required', 422)` and\n`ApiHelper::error('Influencer not found', 404)` \u2014 are passing\n`422`/`404` into the `$data` parameter, not the status code\nparameter, meaning those two responses would return whatever\n`ApiHelper::error()`'s *default* status code is, not 422/404 as\nintended. This method (`campaignBriefCallback`) does **not**\nhave this issue \u2014 its error calls correctly include the `[]`\nplaceholder. Worth checking `ApiHelper::error()`'s actual\nsignature to confirm, but the inconsistency between these two\nmethods in the same file, calling the same helper, is real and\nvisible directly in the code as written.",
                "summary": "Campaign Brief",
                "tags": [
                    "AiSync"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "campaign_id": {
                                        "type": "string"
                                    },
                                    "brief_text": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Campaign not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "anyOf": [
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "brief_text must be a non-empty string"
                                                },
                                                "errors": {
                                                    "type": "array",
                                                    "items": {
                                                        "type": "string"
                                                    },
                                                    "minItems": 0,
                                                    "maxItems": 0,
                                                    "additionalItems": false
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "campaign_id is required"
                                                },
                                                "errors": {
                                                    "type": "array",
                                                    "items": {
                                                        "type": "string"
                                                    },
                                                    "minItems": 0,
                                                    "maxItems": 0,
                                                    "additionalItems": false
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        }
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/logout": {
            "post": {
                "operationId": "auth.logout",
                "description": "Revokes only the bearer token used to authenticate the current\nrequest, ending this device's session. Tokens issued to the same\nuser on other devices are unaffected and remain valid until they\nare individually revoked or expire on their own.",
                "summary": "Logout user",
                "tags": [
                    "Authentication"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/send-otp": {
            "post": {
                "operationId": "auth.sendOtp",
                "description": "Generates a six-digit one-time password and emails it to the\ngiven address. The same endpoint is reused for four distinct\npurposes \u2014 `register`, `login`, `payment`, and `set-2fa` \u2014 and\nthe `purpose` value is bound into the OTP record, so a code\ngenerated for one purpose cannot be redeemed for another even if\nthe email and code match.\n\nEach OTP has a limited validity window and is single-use; a new\n`sendOtp` call for the same email/purpose invalidates any\npreviously issued, unconsumed code rather than allowing several\nvalid codes to coexist.\n\nThis endpoint does not require the email to belong to an\nexisting account \u2014 that distinction (e.g. \"no account found\")\nis left to the corresponding `verifyOtp` call, where it can be\nsurfaced for `login`/`payment`/`set-2fa` without leaking\nexistence information at this earlier step for `register`.",
                "summary": "Send OTP verification code",
                "tags": [
                    "Authentication"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "email": {
                                        "type": "string",
                                        "format": "email"
                                    },
                                    "purpose": {
                                        "type": "string",
                                        "enum": [
                                            "register",
                                            "login",
                                            "payment",
                                            "set-2fa"
                                        ]
                                    }
                                },
                                "required": [
                                    "email",
                                    "purpose"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "anyOf": [
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "No account found for the provided email."
                                                },
                                                "errors": {
                                                    "type": "integer",
                                                    "const": 404
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "Email already verified. Please log in instead."
                                                },
                                                "errors": {
                                                    "type": "integer",
                                                    "const": 400
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                },
                "security": []
            }
        },
        "/verify-otp": {
            "post": {
                "operationId": "auth.verifyOtp",
                "description": "Checks the six-digit code against the most recent unconsumed OTP\nissued for this exact `email` + `purpose` pair. On success, the\ncode is marked consumed (so it cannot be replayed) and the\nservice performs the action tied to that purpose:\n\n- `register` \u2014 confirms the email for a new account and allows\n  registration to proceed.\n- `login` \u2014 authenticates the user and returns a bearer token,\n  used as a passwordless or second-factor login path.\n- `payment` \u2014 authorizes a pending payment action that required\n  step-up verification.\n- `set-2fa` \u2014 confirms ownership of the email before enabling\n  two-factor authentication on the account.\n\nAn incorrect code, an expired code, or a code for a different\npurpose are all rejected; the service is responsible for rate\nlimiting repeated verification attempts against the same code.",
                "summary": "Verify OTP code",
                "tags": [
                    "Authentication"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "email": {
                                        "type": "string",
                                        "format": "email"
                                    },
                                    "otp": {
                                        "type": "string"
                                    },
                                    "purpose": {
                                        "type": "string",
                                        "enum": [
                                            "register",
                                            "login",
                                            "payment",
                                            "set-2fa"
                                        ]
                                    }
                                },
                                "required": [
                                    "email",
                                    "otp",
                                    "purpose"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "anyOf": [
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "Your account has been suspended. Please contact support."
                                                },
                                                "errors": {
                                                    "type": "integer",
                                                    "const": 403
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "No account found for this email"
                                                },
                                                "errors": {
                                                    "type": "integer",
                                                    "const": 404
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "Invalid or expired OTP"
                                                },
                                                "errors": {
                                                    "type": "integer",
                                                    "const": 400
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                },
                "security": []
            }
        },
        "/campaigns/apply": {
            "post": {
                "operationId": "campaignApplication.store",
                "description": "Creates a new application linking an influencer to a campaign.\nThe `influencer_id` is taken from the request body rather than\nderived from the authenticated user, so the `create` policy is\nwhat actually enforces that a caller can only apply on behalf of\ntheir own influencer profile \u2014 both `campaign_id` and\n`influencer_id` only need to reference *existing* records at the\nvalidation layer, not specifically the caller's own.\n\nValidation here does not prevent duplicate applications to the\nsame campaign, or applications to a campaign that is `paused`,\n`completed`, or otherwise closed to new applicants \u2014 if those\nrules exist, they live inside the `create` policy or the service,\nnot in this method's request rules.",
                "summary": "Submit campaign application",
                "tags": [
                    "Campaign Applications"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "campaign_id": {
                                        "type": "string"
                                    },
                                    "influencer_id": {
                                        "type": "integer"
                                    }
                                },
                                "required": [
                                    "campaign_id",
                                    "influencer_id"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/AuthorizationException"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/applications": {
            "get": {
                "operationId": "campaignApplication.index",
                "description": "Returns a different result set depending on who's asking, with\nthe actual scoping logic delegated entirely to\n`CampaignApplicationService::index()` rather than expressed here:\na brand user would reasonably expect applications to their own\ncampaigns, an influencer their own submitted applications, and\nan admin (if one exists) potentially everything. The `viewAny`\npolicy gates whether the endpoint can be called at all, but the\nper-role filtering of *which* applications come back happens\ndownstream in the service.",
                "summary": "Get campaign applications",
                "tags": [
                    "Campaign Applications"
                ],
                "responses": {
                    "200": {
                        "description": "Array of `CampaignApplicationResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/CampaignApplicationResource"
                                            }
                                        }
                                    },
                                    "required": [
                                        "data"
                                    ]
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/AuthorizationException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/applications/{application}": {
            "get": {
                "operationId": "campaignApplication.show",
                "description": "Retrieves a single application by its route-bound model. The\n`view` policy is the only thing standing between this and an\nIDOR-style leak \u2014 since the ID is a plain route parameter with\nno ownership filtering in the query itself, every authorization\ndecision rests on that policy correctly checking that the\ncaller is either the applying influencer or the owning brand.",
                "summary": "Get specific campaign application",
                "tags": [
                    "Campaign Applications"
                ],
                "parameters": [
                    {
                        "name": "application",
                        "in": "path",
                        "required": true,
                        "description": "The application ID",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "`CampaignApplicationResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/CampaignApplicationResource"
                                        }
                                    },
                                    "required": [
                                        "data"
                                    ]
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/AuthorizationException"
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/applications/{application}/withdraw": {
            "patch": {
                "operationId": "campaignApplication.withdraw",
                "description": "Lets an influencer pull back their own application \u2014 typically\nbefore it's been reviewed, though that constraint (if any) is\nenforced by the `withdraw` policy or the service, not visible\nhere. This is a soft state change rather than a deletion: the\napplication record persists with an updated status, so brands\ncan still see that an influencer applied and later withdrew,\nrather than the application disappearing outright.",
                "summary": "Withdraw campaign application",
                "tags": [
                    "Campaign Applications"
                ],
                "parameters": [
                    {
                        "name": "application",
                        "in": "path",
                        "required": true,
                        "description": "The application ID",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/AuthorizationException"
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/billboard/campaigns/applications": {
            "get": {
                "operationId": "campaignApplicationApi.getAllApplications",
                "description": "The platform-wide application queue \u2014 every application across\nevery campaign and every brand, paginated at a fixed 10/page,\nnewest first. This is the closest thing in either\n`CampaignApplication` controller to a single moderation inbox;\nthe v1 controller's `index()` is explicitly per-role-scoped\ninside the service and never returns a cross-brand view like\nthis one does.",
                "summary": "Get all applications",
                "tags": [
                    "Campaign Applications"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/campaigns/{campaign}/applications": {
            "get": {
                "operationId": "campaignApplicationApi.getCampaignApplications",
                "description": "All applications for one specific campaign, route-model-bound\nby `Campaign $campaign` \u2014 the admin view of \"who applied to\n     * this campaign,\" paginated at a fixed 10/page with no override\nparameter (contrast with `TransactionsController::index()`,\nwhich accepts a caller-supplied `per_page`). Eager-loads\n`campaign.brand` (redundant here, since the campaign is already\nknown from the route) and the applicant's `influencer.user.role`\nand `influencer.user.brand` \u2014 that last relation in particular\nis only meaningful if a user can hold both an influencer and a\nbrand profile simultaneously (see the role-switching behavior\ndocumented in `UserController::updateProfile()`), which is\nexactly the scenario this eager-load seems built to surface for\nan admin reviewing an application.",
                "summary": "Get campaign applications",
                "tags": [
                    "Campaign Applications"
                ],
                "parameters": [
                    {
                        "name": "campaign",
                        "in": "path",
                        "required": true,
                        "description": "The campaign ID",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    }
                }
            }
        },
        "/billboard/campaigns/applications/{application}": {
            "get": {
                "operationId": "campaignApplicationApi.show",
                "description": "One application by route-bound ID, with the same relation set\nas `getCampaignApplications()`. Unlike\n`v1\\CampaignApplicationController::show()`, which gates access\nwith the `view` policy (since a brand or influencer should only\nsee applications relevant to them), this method applies no\nper-request access check at all \u2014 appropriate for an\nadmin-only audience that should legitimately be able to view\nany application, but it means this method's safety depends\nentirely on the route-group middleware mentioned above actually\nbeing in place; there is nothing in the method itself limiting\nwhich application IDs can be requested.",
                "summary": "Get single application",
                "tags": [
                    "Campaign Applications"
                ],
                "parameters": [
                    {
                        "name": "application",
                        "in": "path",
                        "required": true,
                        "description": "The application ID",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    }
                }
            }
        },
        "/billboard/campaigns/applications/{application}/accept": {
            "post": {
                "operationId": "campaignApplicationApi.acceptApplication",
                "description": "Delegates the actual status transition and any side effects\n(e.g. generating deliverables, notifying the influencer) to\n`CampaignApplicationService::acceptApplication()` \u2014 this\nmethod itself does no validation and takes no input beyond the\nroute-bound application, so whatever business rules govern\n*when* an application is eligible to be accepted (e.g. only\nfrom a `pending` status, not from an already-`withdrawn` one)\nlive entirely in the service rather than here.",
                "summary": "Accept application",
                "tags": [
                    "Campaign Applications"
                ],
                "parameters": [
                    {
                        "name": "application",
                        "in": "path",
                        "required": true,
                        "description": "The application ID",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    }
                }
            }
        },
        "/billboard/campaigns/applications/{application}/reject": {
            "post": {
                "operationId": "campaignApplicationApi.rejectApplication",
                "description": "Same delegation pattern as `acceptApplication()`, with one\ndifference worth flagging: `reason` is read via\n`$request->input('reason')` with **no validation at all** \u2014 not\n`required`, not a `string` type check, nothing. An admin can\nreject an application with no reason provided (`null` is passed\nstraight through to the service), and there's no length limit\nor sanitization applied at this layer if a reason is given.\nWhatever the service does with a `null` reason \u2014 store it as\nempty, throw, or substitute a default \u2014 is not visible from\nthis method.",
                "summary": "Reject application",
                "tags": [
                    "Campaign Applications"
                ],
                "parameters": [
                    {
                        "name": "application",
                        "in": "path",
                        "required": true,
                        "description": "The application ID",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "reason": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    }
                }
            }
        },
        "/billboard/campaigns/pending-briefs": {
            "get": {
                "operationId": "campaignBrief.pendingBriefs",
                "description": "The admin's brief-review queue: every campaign currently sitting\nat `brief_status = 'pending_review'`, i.e. Django has returned a\ndraft and it's waiting on a human. Notably, a brief in\n`generating` status (webhook fired, no response yet) does not\nshow up here \u2014 only `getBrief()` for a specific ID would reveal\nthat state \u2014 so this queue can understate how many briefs are\nactually \"in flight,\" only surfacing the ones ready for action.",
                "summary": "List campaigns pending brief review",
                "tags": [
                    "Campaign Brief Management (Admin)"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/campaigns/{id}/brief": {
            "get": {
                "operationId": "campaignBrief.getBrief",
                "description": "Returns the raw `ai_brief_draft` text alongside the current\n`brief_status`, regardless of what that status actually is \u2014\nunlike `pendingBriefs()`, this isn't restricted to\n`pending_review` campaigns, so it doubles as a way to inspect a\nbrief that's `generating` (likely still empty/stale), already\n`approved`, or in any other state the field can hold.",
                "summary": "Get the AI brief draft for a specific campaign",
                "tags": [
                    "Campaign Brief Management (Admin)"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Campaign not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            },
            "put": {
                "operationId": "campaignBrief.updateBrief",
                "description": "Lets an admin rewrite the AI-generated text wholesale before\napproving \u2014 `brief_text` replaces `ai_brief_draft` entirely\n(there's no partial/diff editing of individual deliverables\ndespite the method summary's \"tweak individual deliverables\"\nwording; at this stage the brief is still plain text, and\nstructured `ApplicationDeliverable` rows don't exist yet \u2014 see\n`approveBrief()`'s docblock for where that text-to-structured\ngap actually gets bridged, or rather, doesn't, automatically).\n\nOnly callable while `brief_status` is `pending_review` or\n`generating` \u2014 editing a brief that's already `approved`\nis rejected with a 422, so there's no path through this method\nto silently alter a brief influencers have already been shown.\n\nUses `updateQuietly()` rather than `update()`, which suppresses\nEloquent model events \u2014 meaning `CampaignObserver` (the same\nobserver `syncAi()` relies on `touch()` to fire) does **not**\nfire here. An admin's manual brief edit does not trigger an AI\nembedding resync, unlike essentially every other write to a\n`Campaign` row elsewhere in this API.",
                "summary": "Update the brief draft (admin edits before approving)",
                "tags": [
                    "Campaign Brief Management (Admin)"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "brief_text": {
                                        "type": "string",
                                        "minLength": 10
                                    }
                                },
                                "required": [
                                    "brief_text"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Campaign not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/campaigns/{id}/brief/approve": {
            "post": {
                "operationId": "campaignBrief.approveBrief",
                "description": "Only callable from `pending_review`, with a guard against an\nempty draft \u2014 both `generating` (no draft has arrived) and\n`approved` (already done) are rejected with a 422 naming the\nactual current status, which is more diagnostic than most other\n422s in this API (most just say \"cannot be edited\"/\"cannot be\napproved\" without stating the blocking status).\n     *\n     * The docblock's own numbered list (\"1. Creates\nApplicationDeliverable records... 2. Sets brief_status =\n'approved'\") describes intended behavior that **the method body\n     * does not currently implement** \u2014 there is no\n     * `ApplicationDeliverable::create(...)` call anywhere in this\n     * method; it only calls `updateQuietly(['brief_status' =>\n     * 'approved'])`. The inline comment directly above that line\n     * explicitly says deliverables are created via \"the separate\n'Add Deliverables' endpoint\" (i.e.\n`Api\\CampaignApiController::createCampaignDeliverables()` /\n`aiCreateCampaignDeliverables()`), which contradicts the\nmethod's own summary docblock claiming this endpoint does it.\nWorth resolving which of the two comments is stale before\nrelying on either as documentation \u2014 right now they describe\ntwo different system behaviors for the same method.\n\nLike `updateBrief()`, uses `updateQuietly()`, so approving a\nbrief does not fire `CampaignObserver` either.",
                "summary": "Approve the AI brief",
                "tags": [
                    "Campaign Brief Management (Admin)"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "anyOf": [
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "No brief draft found to approve"
                                                },
                                                "errors": {
                                                    "type": "array",
                                                    "items": {
                                                        "type": "string"
                                                    },
                                                    "minItems": 0,
                                                    "maxItems": 0,
                                                    "additionalItems": false
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string"
                                                },
                                                "errors": {
                                                    "type": "array",
                                                    "items": {
                                                        "type": "string"
                                                    },
                                                    "minItems": 0,
                                                    "maxItems": 0,
                                                    "additionalItems": false
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Campaign not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/campaigns/{id}/brief/regenerate": {
            "post": {
                "operationId": "campaignBrief.regenerateBrief",
                "description": "The admin-triggered twin of what\n`v1\\CampaignController::syncAi()` does automatically when a\ncampaign's brief isn't yet approved \u2014 both ultimately POST to\nthe same Django webhook\n(`{ai_url}/webhooks/campaign-brief-requested/`) with the\ncampaign's full attributes and brand data merged in, and both\nset `brief_status` to `generating` on a successful call. The\ndifference is that `syncAi()` fires this as a side effect of an\nembedding resync that may not have been about the brief at all,\nwhile this method exists purely to explicitly force\nregeneration \u2014 e.g. after `updateBrief()`'s admin edits weren't\ngood enough, or after the underlying campaign details changed\nenough to warrant a fresh AI pass.\n\nThree distinct failure modes are each handled with a different,\nspecific status code rather than one generic error: missing AI\nservice configuration returns `503` immediately without\nattempting the call; a non-success HTTP response from the AI\nservice (reached, but errored) returns `502`; and a thrown\nexception (timeout, DNS failure, connection refused \u2014 the\nrequest times out after 5 seconds) is caught separately and\nalso returns `503`. Both the `502` and exception paths log at\n`warning` rather than `error`, despite representing a complete\nfailure of this admin action \u2014 `error`-level logging in this\nfile is reserved for nothing, since this controller has no\n`Log::error()` calls at all, in contrast to\n`TiktokAuthController`, which logs at `error` for analogous\nexternal-API failures.\n\nOn success, sets `brief_status` to `generating` \u2014 note this\nuses `updateQuietly()` here too, so like the other two\nbrief-mutating methods in this controller, `CampaignObserver`\ndoes not fire as a result of this call either, despite this\nmethod being the one that most directly mirrors what\n`syncAi()` does (which *does* fire the observer, via `touch()`,\nas its primary purpose).",
                "summary": "Regenerate the AI brief for a campaign",
                "tags": [
                    "Campaign Brief Management (Admin)"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "503": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "anyOf": [
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "Could not reach AI service"
                                                },
                                                "errors": {
                                                    "type": "array",
                                                    "items": {
                                                        "type": "string"
                                                    },
                                                    "minItems": 0,
                                                    "maxItems": 0,
                                                    "additionalItems": false
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "AI service is not configured"
                                                },
                                                "errors": {
                                                    "type": "array",
                                                    "items": {
                                                        "type": "string"
                                                    },
                                                    "minItems": 0,
                                                    "maxItems": 0,
                                                    "additionalItems": false
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "502": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "AI service returned an error \u2014 try again shortly"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Campaign not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/campaigns": {
            "post": {
                "operationId": "campaign.store",
                "description": "Creates a campaign as either `paid` or `service` type, with\nvalidation branching on that choice via `required_if`:\n\n- `paid` campaigns must specify a `budget` and a `payment_plan`\n  (`fixed` or `performance`); `fixed` additionally requires\n  `payment_price_per_creator`, `performance` requires\n  `payment_price_per_view`.\n- `service` campaigns instead require a `service_type` and carry\n  no budget or payment plan at all.\n\n`required_content_types` and `required_content_styles` are each\nvalidated against a fixed enum (e.g. `reels`, `tutorial`,\n`ugc`) \u2014 values outside those lists are rejected outright\nrather than stored as free text, so the campaign's content\nrequirements stay machine-filterable for matching influencers.\n\nThe cover image, if provided, is stored on the `public` disk\nbefore the campaign is created; if campaign creation itself\nfails after the file has been stored, the uploaded file is not\nautomatically cleaned up by this method.",
                "summary": "Create a new campaign",
                "tags": [
                    "Campaign Management"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "title": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "description": {
                                        "type": "string"
                                    },
                                    "products": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    },
                                    "dos_for_creators": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "donts_for_creators": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "preferred_platforms": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "requirements": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "industry": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "budget": {
                                        "type": [
                                            "number",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "payment_plan": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "enum": [
                                            "fixed",
                                            "performance"
                                        ]
                                    },
                                    "payment_price_per_creator": {
                                        "type": [
                                            "number",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "payment_price_per_view": {
                                        "type": [
                                            "number",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "cover_image": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "binary",
                                        "contentMediaType": "application/octet-stream",
                                        "maxLength": 2048
                                    },
                                    "target_audience_description": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "target_audience_age_range": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 100
                                    },
                                    "target_audience_gender": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "enum": [
                                            "male",
                                            "female",
                                            "all",
                                            "non-binary"
                                        ]
                                    },
                                    "timeline_start_date": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "date-time"
                                    },
                                    "timeline_end_date": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "date-time"
                                    },
                                    "campaign_goals": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    },
                                    "primary_cta": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "website_url": {
                                        "type": "string",
                                        "format": "uri"
                                    },
                                    "required_hashtags": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "required_mentions": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "deliverables": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    },
                                    "status": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 50
                                    },
                                    "campaign_type": {
                                        "type": "string",
                                        "enum": [
                                            "paid",
                                            "service"
                                        ]
                                    },
                                    "service_type": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "required_content_types": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string",
                                            "enum": [
                                                "reels",
                                                "short_video",
                                                "long_video",
                                                "story",
                                                "static_post",
                                                "carousel",
                                                "live",
                                                "podcast",
                                                "blog"
                                            ]
                                        }
                                    },
                                    "required_content_styles": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string",
                                            "enum": [
                                                "storytelling",
                                                "review",
                                                "reaction",
                                                "tutorial",
                                                "dance",
                                                "lifestyle_vlog",
                                                "unboxing",
                                                "comedy",
                                                "educational",
                                                "interview",
                                                "challenge",
                                                "ugc"
                                            ]
                                        }
                                    }
                                },
                                "required": [
                                    "title",
                                    "description",
                                    "primary_cta",
                                    "website_url",
                                    "campaign_type"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/AuthorizationException"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            },
            "get": {
                "operationId": "campaign.index",
                "description": "Returns the public, browsable campaign feed \u2014 every campaign\nregardless of which brand owns it, intended for influencers\ndiscovering opportunities rather than a brand managing its own.\nFiltering, ordering, and pagination are all delegated to\n`CampaignService::index()`; nothing about the result set is\nscoped or restricted in this method itself, so any access\ncontrol on which campaigns are visible here (e.g. excluding\n`draft` campaigns from other brands) has to live inside that\nservice call.",
                "summary": "Get all campaigns",
                "tags": [
                    "Campaign Management"
                ],
                "responses": {
                    "200": {
                        "description": "Array of `CampaignResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/CampaignResource"
                                            }
                                        }
                                    },
                                    "required": [
                                        "data"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/campaigns/my-brand": {
            "get": {
                "operationId": "campaign.myBrandCampaigns",
                "description": "Returns every campaign owned by the authenticated user's brand,\nregardless of status \u2014 unlike `index()`, this is the\nbrand-management view, not the public discovery feed, so\n`draft` and `paused` campaigns are included alongside `active`\nones. Returns a 403 rather than an empty list for users with no\nbrand profile, since \"you have no campaigns\" and \"you can't\n     * have campaigns\" are different conditions the frontend needs to\ndistinguish.",
                "summary": "Get my brand campaigns",
                "tags": [
                    "Campaign Management"
                ],
                "responses": {
                    "200": {
                        "description": "Array of `CampaignResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/CampaignResource"
                                            }
                                        }
                                    },
                                    "required": [
                                        "data"
                                    ]
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "You do not have a brand"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/campaigns/total-active-budget": {
            "get": {
                "operationId": "campaign.totalActiveCampaignBudget",
                "description": "A narrower, lighter-weight sibling of `getDashboardMetrics()` \u2014\nscoped to just budget figures for `active` campaigns. Only\n`paid` campaigns contribute to `paid_campaigns_budget` and\n`total_active_campaign_budget`, since `service` campaigns have\nno `budget` value by design; `service_campaigns_count` exists\nso the frontend can still represent service campaigns in\ncontext even though they add nothing to the budget totals.",
                "summary": "Get total active campaign budget",
                "tags": [
                    "Campaign Management"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "You do not have a brand"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/campaigns/dashboard-metrics": {
            "get": {
                "operationId": "campaign.getDashboardMetrics",
                "description": "The full brand dashboard summary: campaign counts by every\nstatus, total budget across *all* campaigns regardless of\nstatus (unlike `totalActiveCampaignBudget()`, which is\nactive-only), and an estimated `total_reach`.\n\n`total_reach` is computed per campaign as\n`(budget \u00f7 payment_price_per_creator) \u00d7 5000`, summed across\nevery campaign that has both a positive budget and a positive\nper-creator price \u2014 `performance`-plan campaigns priced per\nview rather than per creator are silently excluded from this\nfigure rather than estimated some other way, since the formula\nhas no equivalent term for `payment_price_per_view`. The `5000`\nmultiplier is a fixed assumption about average reach per\ncreator and is not derived from any campaign-specific data.",
                "summary": "Get dashboard metrics",
                "tags": [
                    "Campaign Management"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "You do not have a brand"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/campaigns/{campaign}": {
            "get": {
                "operationId": "campaign.show",
                "description": "Retrieves a single campaign by route-bound ID, with its `brand`\nrelation eagerly loaded so the response never triggers an\nadditional query per request for brand data. As with\n`CampaignApplicationController::show()`, the model is route-bound\nwith no ownership filter in the query itself \u2014 the `view` policy\nis solely responsible for deciding whether this caller may see\nthis particular campaign (e.g. a `draft` campaign belonging to a\ndifferent brand).",
                "summary": "Get specific campaign details",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "campaign",
                        "in": "path",
                        "required": true,
                        "description": "The campaign ID",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "`CampaignResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/CampaignResource"
                                        }
                                    },
                                    "required": [
                                        "data"
                                    ]
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/AuthorizationException"
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            },
            "put": {
                "operationId": "v1.campaign.update_1",
                "description": "Partial update \u2014 every field uses `sometimes`, so omitted fields\nare left untouched rather than cleared, and only fields actually\npresent in the request are validated and passed through.\nNotably, `campaign_type` itself can be changed via this\nendpoint (`sometimes|in:paid,service`), but the `required_if`\nrules that enforce budget/payment fields on creation are not\nre-applied here \u2014 switching an existing `paid` campaign to\n`service` (or vice versa) does not, by this validation alone,\nforce the now-irrelevant or now-required fields to be\nupdated in the same request.\n\nReplacing the cover image deletes the previous file from the\n`public` disk before storing the new one; if the new upload\nfails after the old file is deleted, the campaign is left\nwithout a cover image rather than retaining the old one.",
                "summary": "Update campaign",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "campaign",
                        "in": "path",
                        "required": true,
                        "description": "The campaign ID",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "requestBody": {
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "title": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "description": {
                                        "type": "string"
                                    },
                                    "products": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    },
                                    "dos_for_creators": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "donts_for_creators": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "preferred_platforms": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "requirements": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "industry": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "budget": {
                                        "type": [
                                            "number",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "payment_plan": {
                                        "type": "string",
                                        "enum": [
                                            "fixed",
                                            "performance"
                                        ]
                                    },
                                    "payment_price_per_creator": {
                                        "type": [
                                            "number",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "payment_price_per_view": {
                                        "type": [
                                            "number",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "cover_image": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "binary",
                                        "contentMediaType": "application/octet-stream",
                                        "maxLength": 2048
                                    },
                                    "target_audience_description": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "target_audience_age_range": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 100
                                    },
                                    "target_audience_gender": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "enum": [
                                            "male",
                                            "female",
                                            "all",
                                            "non-binary"
                                        ]
                                    },
                                    "timeline_start_date": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "date-time"
                                    },
                                    "timeline_end_date": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "date-time"
                                    },
                                    "campaign_goals": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    },
                                    "primary_cta": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "website_url": {
                                        "type": "string",
                                        "format": "uri"
                                    },
                                    "required_hashtags": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "required_mentions": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "deliverables": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    },
                                    "status": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 50
                                    },
                                    "campaign_type": {
                                        "type": "string",
                                        "enum": [
                                            "paid",
                                            "service"
                                        ]
                                    },
                                    "service_type": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "required_content_types": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string",
                                            "enum": [
                                                "reels",
                                                "short_video",
                                                "long_video",
                                                "story",
                                                "static_post",
                                                "carousel",
                                                "live",
                                                "podcast",
                                                "blog"
                                            ]
                                        }
                                    },
                                    "required_content_styles": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string",
                                            "enum": [
                                                "storytelling",
                                                "review",
                                                "reaction",
                                                "tutorial",
                                                "dance",
                                                "lifestyle_vlog",
                                                "unboxing",
                                                "comedy",
                                                "educational",
                                                "interview",
                                                "challenge",
                                                "ugc"
                                            ]
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/AuthorizationException"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            },
            "patch": {
                "operationId": "v1.campaign.update_2",
                "description": "Partial update \u2014 every field uses `sometimes`, so omitted fields\nare left untouched rather than cleared, and only fields actually\npresent in the request are validated and passed through.\nNotably, `campaign_type` itself can be changed via this\nendpoint (`sometimes|in:paid,service`), but the `required_if`\nrules that enforce budget/payment fields on creation are not\nre-applied here \u2014 switching an existing `paid` campaign to\n`service` (or vice versa) does not, by this validation alone,\nforce the now-irrelevant or now-required fields to be\nupdated in the same request.\n\nReplacing the cover image deletes the previous file from the\n`public` disk before storing the new one; if the new upload\nfails after the old file is deleted, the campaign is left\nwithout a cover image rather than retaining the old one.",
                "summary": "Update campaign",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "campaign",
                        "in": "path",
                        "required": true,
                        "description": "The campaign ID",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "requestBody": {
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "title": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "description": {
                                        "type": "string"
                                    },
                                    "products": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    },
                                    "dos_for_creators": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "donts_for_creators": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "preferred_platforms": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "requirements": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "industry": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "budget": {
                                        "type": [
                                            "number",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "payment_plan": {
                                        "type": "string",
                                        "enum": [
                                            "fixed",
                                            "performance"
                                        ]
                                    },
                                    "payment_price_per_creator": {
                                        "type": [
                                            "number",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "payment_price_per_view": {
                                        "type": [
                                            "number",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "cover_image": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "binary",
                                        "contentMediaType": "application/octet-stream",
                                        "maxLength": 2048
                                    },
                                    "target_audience_description": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "target_audience_age_range": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 100
                                    },
                                    "target_audience_gender": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "enum": [
                                            "male",
                                            "female",
                                            "all",
                                            "non-binary"
                                        ]
                                    },
                                    "timeline_start_date": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "date-time"
                                    },
                                    "timeline_end_date": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "date-time"
                                    },
                                    "campaign_goals": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    },
                                    "primary_cta": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "website_url": {
                                        "type": "string",
                                        "format": "uri"
                                    },
                                    "required_hashtags": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "required_mentions": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "deliverables": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    },
                                    "status": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 50
                                    },
                                    "campaign_type": {
                                        "type": "string",
                                        "enum": [
                                            "paid",
                                            "service"
                                        ]
                                    },
                                    "service_type": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "required_content_types": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string",
                                            "enum": [
                                                "reels",
                                                "short_video",
                                                "long_video",
                                                "story",
                                                "static_post",
                                                "carousel",
                                                "live",
                                                "podcast",
                                                "blog"
                                            ]
                                        }
                                    },
                                    "required_content_styles": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string",
                                            "enum": [
                                                "storytelling",
                                                "review",
                                                "reaction",
                                                "tutorial",
                                                "dance",
                                                "lifestyle_vlog",
                                                "unboxing",
                                                "comedy",
                                                "educational",
                                                "interview",
                                                "challenge",
                                                "ugc"
                                            ]
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/AuthorizationException"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            },
            "delete": {
                "operationId": "campaign.destroy",
                "description": "Hard-deletes the campaign row and removes its cover image file\nfrom the `public` disk. Because this is a real deletion rather\nthan a status change (contrast with\n`CampaignApplicationController::withdraw()`, which only changes\nstatus), any applications or submissions tied to this campaign\nlose their parent record \u2014 whether those are cascade-deleted,\norphaned, or blocked entirely depends on the database/service\nlayer and is not handled by this method.",
                "summary": "Delete campaign",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "campaign",
                        "in": "path",
                        "required": true,
                        "description": "The campaign ID",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/AuthorizationException"
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/campaigns/{id}/progress": {
            "get": {
                "operationId": "campaign.showWithProgress",
                "description": "A heavier variant of `show()` that also returns `progress` data\n\u2014 applications received, submissions made, and completion\nstatus \u2014 computed by `CampaignService::show()`. Takes the ID as\na plain `string` and resolves it manually via `findOrFail`\nrather than route-model binding like `show()` does, which means\na missing campaign here surfaces as a generic 404 from\n`findOrFail` rather than a typed model-not-found response; the\ntwo methods are not guaranteed to fail identically for a bad ID.",
                "summary": "Get campaign with progress",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/AuthorizationException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/campaigns/{campaign}/sync-ai": {
            "post": {
                "operationId": "campaign.syncAi",
                "description": "A workaround endpoint: normal campaign edits go through\n`update()`, but an `active` campaign may be locked from regular\nediting (e.g. to protect a live campaign's terms from changing\nmid-flight), leaving no other way to refresh its AI embedding\nafter upstream brand data changes. Calling `touch()` updates\nonly the `updated_at` timestamp \u2014 no campaign field actually\nchanges \u2014 purely to fire `CampaignObserver::updated()`, which\npushes the latest campaign + brand snapshot to the AI engine.\n\nAs a side effect, if the campaign's brief hasn't yet reached\n`approved` status, this also re-fires the brief-generation\nwebhook \u2014 so calling this endpoint can trigger a new brief\ngeneration cycle even when the caller's only intent was to\nresync the embedding.",
                "summary": "Trigger AI sync for a specific campaign",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "campaign",
                        "in": "path",
                        "required": true,
                        "description": "The campaign ID",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "403": {
                        "$ref": "#/components/responses/AuthorizationException"
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/billboard/campaigns": {
            "get": {
                "operationId": "campaignApi.getCampaigns",
                "description": "Returns every campaign in the system with its `brand` relation\nloaded \u2014 no status filter, no pagination, and (per the class-level\nnote above) no scoping to a particular brand or user. Functionally\noverlaps with `v1\\CampaignController::index()`, but that method\ndelegates to `CampaignService::index()` and may apply filtering\nthere; this method queries `Campaign` directly with none.",
                "summary": "Get all campaigns",
                "tags": [
                    "Campaign Management"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/campaigns/search": {
            "get": {
                "operationId": "campaignApi.searchCampaigns",
                "description": "Matches `title`, `description`, or the owning brand's\n`company_name`, each via unanchored `LIKE '%...%'` \u2014 the same\nfull-scan-shaped pattern flagged in `LocationController::search()`,\nhere applied across three columns and one relationship join at\nonce via `orWhereHas`, which is a heavier query than any single\n`LIKE` search elsewhere in this API. No result limit or\npagination is applied; `count` reflects the full match set, not\na page size.\n\nThe three conditions are combined with `where()` followed by\n`orWhere()`/`orWhereHas()` \u2014 note this means a *broader* match\nthan \"title AND description AND brand name all contain query\";\nany one of the three conditions matching is sufficient, which is\nalmost certainly the intended behavior for a search feature, but\nworth confirming the operator precedence reads as intended for\nany future added conditions in this chain.",
                "summary": "Search campaigns",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "query",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "minLength": 1
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/billboard/campaigns/user/{userId}": {
            "get": {
                "operationId": "campaignApi.getCampaignsByUser",
                "description": "Takes a bare `userId` route parameter with no relationship to\nthe authenticated caller at all \u2014 this returns campaigns for\n*any* user ID requested, not \"my campaigns.\" Contrast directly\nwith `v1\\CampaignController::myBrandCampaigns()`, which derives\nthe brand strictly from `auth()->user()`; this method has no\nsuch derivation and trusts the route parameter outright. Absent\nany authorization layer outside this file, this endpoint alone\nwould let any caller enumerate any other user's campaigns by\nincrementing `$userId`.",
                "summary": "Get campaigns by user",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "userId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/campaigns/{id}": {
            "get": {
                "operationId": "campaignApi.getCampaignById",
                "description": "Loads `brand` and `applicationDeliverables` together \u2014 notably\nincluding deliverables, which the v1\n`CampaignController::show()` does not load at all (deliverables\nthere are reached only via `CampaignSubmissionController`). Uses\n`find()` rather than route-model binding or `findOrFail()`,\nreturning a structured 404 through `ApiHelper::error()` rather\nthan relying on Laravel's default model-not-found behavior.",
                "summary": "Get specific campaign",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Campaign not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            },
            "delete": {
                "operationId": "campaignApi.deleteCampaign",
                "description": "A hard delete with **no cover-image cleanup** \u2014 unlike\n`v1\\CampaignController::destroy()`, which explicitly deletes the\ncampaign's cover image from the `public` disk before removing\nthe row, this method calls `$campaign->delete()` directly with\nno `Storage::disk('public')->delete(...)` step at all, leaving\nany cover image as an orphaned file. Also has no visible\nauthorization (consistent with every other method here), and,\nlike `destroy()` in v1, gives no indication of what happens to\ndependent `ApplicationDeliverable`, application, or submission\nrows once their parent campaign is gone.",
                "summary": "Delete campaign",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Campaign not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/campaigns/{id}/approve": {
            "post": {
                "operationId": "campaignApi.approveCampaign",
                "description": "A moderation action: unconditionally sets `status` to `active`\nregardless of the campaign's current status \u2014 there is no check\nthat the campaign was previously `draft` or pending review, so\nthis can be called on an already-active, paused, or even\ndeclined/suspended campaign and will silently move it back to\n`active` every time. This is the kind of endpoint that strongly\nimplies an admin-only audience, which makes the complete absence\nof any authorization check in this file (see class-level note)\nthe single most important thing to verify before this ships as\ndocumentation rather than treating the access model as settled.",
                "summary": "Approve campaign",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Campaign not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/campaigns/{id}/deliverables": {
            "patch": {
                "operationId": "campaignApi.createCampaignDeliverables",
                "description": "Appends new `ApplicationDeliverable` rows to a campaign; does\nnot replace or clear existing ones, so calling this twice with\nthe same payload produces duplicate deliverables rather than\noverwriting. `deliverable_ref_id` is generated as `dlv_` plus\na 1-based sequence number derived from the *current* count of\nexisting deliverables for this campaign at the moment the\nrequest is processed (`$existingCount + $index + 1`) \u2014 this is\nnot wrapped in a transaction or lock, so two concurrent calls to\nthis endpoint for the same campaign could both read the same\n`$existingCount` and generate colliding `deliverable_ref_id`\nvalues, the same class of race condition flagged in\n`TiktokAuthController::generateAccountNumber()` and\n`SocialAuthController::generateAccountNumber()` earlier.\n\n`platform` and `content_type` are read directly from each\ndeliverable array element with no validation rules applied at\nall in this method (only a top-level check that `deliverables`\nitself is present and is an array) \u2014 a malformed element missing\n`platform` or `content_type` would throw an undefined-array-key\nerror rather than return a structured 422, since\n`$deliverable['platform']` is accessed unguarded.",
                "summary": "Create campaign deliverables",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Please provide deliverables in request body."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Campaign not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/campaigns/{id}/decline": {
            "post": {
                "operationId": "campaignApi.declineCampaign",
                "description": "The same unconditional-status-set pattern as `approveCampaign()`\nand `suspendCampaign()` \u2014 sets `status` to `declined` regardless\nof current state, with the same absence of any visible\nauthorization.",
                "summary": "Decline campaign",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Campaign not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/campaigns/{id}/suspend": {
            "post": {
                "operationId": "campaignApi.suspendCampaign",
                "description": "Same pattern again, setting `status` to `suspended`. Between\n`approveCampaign`, `declineCampaign`, and this method, the\ncampaign's status can be moved directly between any two states\nin one call each \u2014 there is no state-machine enforcement (e.g.\npreventing `declined` \u2192 `active` without going through some\nre-review step) anywhere across these three methods.",
                "summary": "Suspend campaign",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Campaign not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboardai-agent/campaigns/{id}/ai-deliverables": {
            "post": {
                "operationId": "campaignApi.aiCreateCampaignDeliverables",
                "description": "Byte-for-byte identical implementation to\n`createCampaignDeliverables()` immediately above \u2014 same\nvalidation (or lack thereof), same sequential ref-numbering\nrace condition, same response shape. The only difference\nbetween the two methods is the name and the docblock; nothing\nin this method's body actually involves AI generation, despite\nthe name and summary suggesting deliverables are AI-produced.\nIf this is meant to be the AI-assisted variant (e.g. accepting\nAI-suggested deliverables from a separate generation step\nupstream of this call) versus the manually-entered variant,\nthat distinction is not expressed anywhere in this controller \u2014\nboth endpoints currently do exactly the same thing.",
                "summary": "AI Create campaign deliverables",
                "tags": [
                    "Campaign Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Please provide deliverables in request body."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Campaign not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/campaigns/{campaignId}/submits": {
            "get": {
                "operationId": "campaignSubmission.getCampaignDeliverables",
                "description": "The entry point an influencer hits before submitting anything \u2014\nit both checks eligibility and returns the data needed to build\na submission form in one call, rather than requiring a separate\n\"can I submit?\" check up front.\n\n`can_submit` is `false` whenever the influencer has no approved\napplication for this campaign, which covers three distinct\nstates the response collapses into one generic 404: never\napplied, applied but still pending review, and applied but\nrejected. The service may supply a more specific\n`brief_message` to distinguish these for the caller; if it\ndoesn't, the controller falls back to the generic \"no approved\n     * application\" message, which means the precision of this error\ndepends entirely on whether the service bothered to set\n`brief_message`.\n\nUnlike `CampaignController::show()` and similar, authorization\nhere is not policy-based \u2014 eligibility is determined entirely\ninside the service via the `can_submit` flag, with no\n`$this->authorize()` call in this method at all.",
                "summary": "Get campaign deliverables",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "campaignId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "anyOf": [
                                                {
                                                    "type": "string"
                                                },
                                                {
                                                    "type": "string",
                                                    "enum": [
                                                        "No approved application found for this campaign."
                                                    ]
                                                }
                                            ]
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            },
            "post": {
                "operationId": "campaignSubmission.storeSubmission",
                "description": "Creates proof-of-content for a specific deliverable on the\ninfluencer's own approved application. `application_deliverable_id`\nonly needs to reference an *existing* deliverable at the\nvalidation layer \u2014 it does not, by itself, confirm that\ndeliverable belongs to an application owned by this influencer\nor to the `campaignId` in the route; that ownership check (if\npresent) happens inside `storeSubmission()` on the service,\nwhich is also where a duplicate submission against an\nalready-fulfilled deliverable would need to be rejected, since\nnothing in this method's validation prevents it.\n\n`private_metrics_submitted` is a self-reported boolean flag, not\na verification \u2014 it records that the influencer claims to have\nalso provided private platform analytics (e.g. Instagram\nInsights screenshots not visible to the public), but nothing\nhere checks that any such data was actually attached.\n\nScreenshot uploads are capped at 5MB each and stored on the\n`public` disk; because each file is stored individually as the\nloop runs, a later file failing validation does not roll back\nfiles already stored earlier in the same request.\n\nFailures from the service surface as a generic 400 with\nwhatever message the service provides \u2014 this method has no\nvisibility into *why* a given attempt failed (e.g. deliverable\nalready submitted, campaign closed, application withdrawn) and\ncannot distinguish those cases in the HTTP status code.",
                "summary": "Store new submission",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "campaignId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "application_deliverable_id": {
                                        "type": "string",
                                        "format": "uuid"
                                    },
                                    "post_url": {
                                        "type": "string",
                                        "format": "uri"
                                    },
                                    "platform": {
                                        "type": "string"
                                    },
                                    "private_metrics_submitted": {
                                        "type": "boolean"
                                    },
                                    "influencer_screenshots": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string",
                                            "format": "binary",
                                            "contentMediaType": "application/octet-stream",
                                            "maxLength": 5120
                                        }
                                    }
                                },
                                "required": [
                                    "application_deliverable_id",
                                    "post_url",
                                    "platform"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/submissions/{id}": {
            "get": {
                "operationId": "v1.campaignSubmission.getSubmission",
                "description": "Retrieves a single submission by ID with no ownership or brand\nscoping at all in this method \u2014 unlike `getInfluencerSubmissions()`,\nwhich is explicitly scoped to `Auth::user()->influencer->id`,\nthis endpoint takes a bare integer ID and returns whatever\n`getSubmission()` finds, with no policy check and no comparison\nagainst the requesting user's identity. Whether this is\nintentionally public (e.g. a brand needs to view any submission\nto their campaign) or a gap depends entirely on what\n`CampaignSubmissionService::getSubmission()` does internally,\nwhich is not visible from this controller.",
                "summary": "Get submission details",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Submission not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            },
            "delete": {
                "operationId": "campaignSubmission.deleteSubmission",
                "description": "Passes both the submission `$id` and the caller's own\n`$influencerId` into the service, which means \u2014 unlike\n`getSubmission()` above \u2014 ownership scoping is explicit here:\nthe service is expected to verify the submission actually\nbelongs to this influencer before deleting it, rather than\ndeleting by ID alone. What happens to a submission already\nreviewed or approved by the brand (i.e. whether deletion is\nstill allowed post-review) is determined entirely by the\nservice's `deleteSubmission()` logic, not by this method.",
                "summary": "Delete submission",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/submissions": {
            "get": {
                "operationId": "campaignSubmission.getInfluencerSubmissions",
                "description": "Returns every submission the authenticated influencer has ever\nmade, across all campaigns \u2014 not scoped to a single\n`campaignId` the way `getCampaignDeliverables()` is. Useful for\na \"my submissions\" history view rather than a per-campaign\nstatus check.",
                "summary": "Get influencer submissions",
                "tags": [
                    "Campaign Submissions"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/billboard/campaigns/{campaignId}/submissions": {
            "get": {
                "operationId": "campaignSubmission.getCampaignSubmissions",
                "description": "Every submission for one campaign, across all influencers and\napplications \u2014 the admin view of \"what's been submitted against\n     * this campaign so far.\" Takes a bare `int $campaignId` with no\nexistence check on the campaign itself (no `Campaign::findOrFail`\nor equivalent) \u2014 an invalid campaign ID likely just returns an\nempty `submissions` array rather than a 404, since the absence\nof matches and the absence of the campaign itself aren't\ndistinguished here.",
                "summary": "Get campaign submissions",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "campaignId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/submissions/{id}": {
            "get": {
                "operationId": "campaignSubmission.getSubmission",
                "description": "A single submission by ID, with no ownership scoping \u2014 which is\nthe correct, expected shape for an admin endpoint (an admin\nshould be able to view any submission). See the class-level note\nabove for why this method's existence here is also relevant to\na finding in the v1 controller of the same name.",
                "summary": "Get submission details",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Submission not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/influencers/{influencerId}/submissions": {
            "get": {
                "operationId": "campaignSubmission.getInfluencerSubmissionsAdmin",
                "description": "The admin view of one influencer's full submission history\nacross every campaign \u2014 same underlying service call as\n`v1\\CampaignSubmissionController::getInfluencerSubmissions()`,\nbut taking an explicit `influencerId` route parameter here\nrather than deriving it from `Auth::user()->influencer->id` \u2014\nthe v1 version can only ever return the authenticated caller's\nown submissions; this one can return any influencer's, by\ndesign, for admin review.",
                "summary": "Get influencer submissions",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "influencerId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/submissions/{submissionId}/approve": {
            "post": {
                "operationId": "campaignSubmission.approveInfluencerSubmission",
                "description": "The most consequential method in this controller \u2014 approving a\nsubmission triggers a five-step cascade, all performed\nsynchronously in this one request with no visible database\ntransaction wrapping them:\n\n1. Sets the submission's own `progression_status` to `active`.\n2. Increments `total_deliverables_approved` directly on the\n   parent `CampaignApplication` \u2014 a counter maintained by\n   increment rather than recomputed from a count query, so it's\n   only as accurate as every write path that's supposed to\n   increment/decrement it actually doing so consistently.\n3. Recalculates the application's overall completion percentage\n   based on that counter.\n4. Creates the initial progression-tracking record for this\n   submission (presumably what downstream progress reporting \u2014\n   e.g. `v1\\CampaignController::showWithProgress()` \u2014 reads\n   from).\n5. Updates the related `ApplicationDeliverable`'s status based\n   on this submission, and creates a notification for the\n   influencer.\n\nBecause none of this is wrapped in `DB::transaction()`, a\nfailure partway through (e.g. step 4 throwing) would leave the\nsubmission marked `active` and the counter incremented, but no\nprogression record created and no notification sent \u2014 a\npartially-approved submission with no way for the influencer to\nknow about it. There is also no check on the submission's\n*current* `progression_status` before approving it \u2014 this can\nbe called on an already-`active` or even already-`rejected`\nsubmission, which would re-increment\n`total_deliverables_approved` a second time for the same\nsubmission if called twice, double-counting it in the\ncompletion percentage.",
                "summary": "Approve submission",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "submissionId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Submission not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/submissions/{submissionId}/reject": {
            "post": {
                "operationId": "campaignSubmission.rejectInfluencerSubmission",
                "description": "Requires a `rejection_reason` (1\u2013500 chars) \u2014 unlike\n`Api\\CampaignApplicationApiController::rejectApplication()`,\nwhose `reason` field has no validation at all, this sibling\nmethod on a structurally similar action does validate its\nreason field properly. Worth using this method's validation\nrule as the fix for that other one, since they're clearly meant\nto serve the same kind of purpose.\n\nSets `progression_status` to `rejected` and notifies the\ninfluencer with the reason \u2014 but unlike `approveInfluencerSubmission()`,\ndoes **not** touch `total_deliverables_approved` or the\napplication's completion percentage at all, which is the\ncorrect behavior for a rejection (nothing was approved, so\nnothing should count toward completion) but means the two\nsibling methods have meaningfully different side-effect\nfootprints worth knowing about together rather than assuming\nsymmetry between \"approve\" and \"reject.\"\n\nLike `approveInfluencerSubmission()`, has no guard against being\ncalled on a submission that's already `rejected` or already\n`active` \u2014 rejecting an already-approved submission would not\nroll back the increment that approval already made, leaving\n`total_deliverables_approved` overstated relative to the\nsubmission's final (`rejected`) status.",
                "summary": "Reject submission",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "submissionId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "rejection_reason": {
                                        "type": "string",
                                        "maxLength": 500
                                    }
                                },
                                "required": [
                                    "rejection_reason"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Submission not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/billboard/submissions/{submissionId}/progressions": {
            "get": {
                "operationId": "submissionProgression.index",
                "description": "Every recorded check for one submission, in check order\n(`check_number` ascending \u2014 i.e. chronological by check, not by\n`checked_at`, though under normal sequential use the two should\nagree). Eager-loads the parent `campaignSubmission`, unlike\n`SubmissionCheckReminderController::index()`, which doesn't load\nits parent submission at all \u2014 a minor inconsistency between\ntwo controllers covering closely related data. Same\nmissing-submission-existence-check gap as\n`SubmissionCheckReminderController::index()`: an invalid\n`submissionId` returns an empty list rather than a 404.",
                "summary": "Get submission progressions",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "submissionId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "operationId": "submissionProgression.store",
                "description": "Records one engagement snapshot (likes, comments, views,\nimpressions, reach, shares, saves, engagement rate, follower\ncount at time of check) plus admin remarks and screenshots \u2014\nthis is the actual data point `Api\\CampaignSubmissionController`\nand downstream campaign-performance reporting would read from,\nin contrast to `SubmissionCheckReminder`, which carries no\nmetrics at all, only a due date.\n\n**`check_number` is computed by reading the current max and\nadding one \u2014 `MAX(check_number) + 1` \u2014 with no lock, no unique\nconstraint enforced at this layer, and no transaction wrapping\nthe read-then-write.** This is the exact same class of race\ncondition flagged repeatedly across this codebase (account\nnumbers in `TiktokAuthController`/`SocialAuthController`,\ndeliverable ref numbers in `CampaignApiController`), but here\nit's arguably the most consequential instance: two admins\nrecording a check for the same submission at nearly the same\nmoment would both read the same `$lastCheckNumber`, and both\ninserts would succeed with the same `check_number` value unless\na database-level unique constraint on\n(`campaign_submission_id`, `check_number`) exists and this\nmethod is prepared to handle that failure \u2014 it currently isn't.\nThe result would be two different progression records both\nclaiming to be \"check #3,\" with `index()`'s ordering unable to\ndistinguish which actually happened first beyond whatever\n`checked_at`/`id` tiebreak the database happens to apply.\n\n`is_final` is accepted and stored but, as with\n`SubmissionCheckReminder::is_final_check`, not acted on anywhere\nin this method \u2014 marking a check as final doesn't, by itself,\nclose out the submission's monitoring, cancel any pending\n`SubmissionCheckReminder` rows for it, or trigger any\nnotification; whatever significance `is_final = true` carries\nlives entirely outside this method, if it exists at all.",
                "summary": "Create progression check",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "submissionId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "likes": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "comments": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "follower_count_at_check": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "views": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "impressions": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "reach": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "shares": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "saves": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "engagement_rate": {
                                        "type": [
                                            "number",
                                            "null"
                                        ],
                                        "minimum": 0,
                                        "maximum": 100
                                    },
                                    "admin_remarks": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "admin_screenshots": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    },
                                    "is_final": {
                                        "type": "boolean"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/billboard/progressions/{id}": {
            "put": {
                "operationId": "submissionProgression.update",
                "description": "A full field-level update of an existing check \u2014 every metric\nfield is independently `sometimes`-equivalent via\n`nullable`/optional rules, so an admin can correct, say, just\n`likes` after the fact without resupplying every other field.\nNotably, `check_number` is **not** one of the editable fields\nhere \u2014 it's set once at creation in `store()` and this method\nhas no path to change it, so correcting a sequencing mistake\ncaused by the race condition above (two checks sharing the same\nnumber) can't be fixed by editing the conflicting row's number\nthrough this endpoint; it would require a direct database\nchange or a delete-and-recreate.",
                "summary": "Update progression check",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "likes": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "comments": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "follower_count_at_check": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "views": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "impressions": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "reach": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "shares": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "saves": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ],
                                        "minimum": 0
                                    },
                                    "engagement_rate": {
                                        "type": [
                                            "number",
                                            "null"
                                        ],
                                        "minimum": 0,
                                        "maximum": 100
                                    },
                                    "admin_remarks": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "admin_screenshots": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    },
                                    "is_final": {
                                        "type": "boolean"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Progression not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            },
            "delete": {
                "operationId": "submissionProgression.destroy",
                "description": "A hard delete with no renumbering of the remaining checks'\n`check_number` sequence \u2014 deleting check #2 out of a five-check\nhistory leaves checks 1, 3, 4, 5 in place with a gap at 2,\nrather than shifting 3\u20135 down. That's very likely the *correct*\nbehavior (check numbers should represent \"the Nth time this was\n     * actually checked,\" not \"position in a re-sorted list\"), but\ncombined with `store()`'s race condition above, it means gaps\nand *duplicates* in the `check_number` sequence are both\npossible outcomes over a submission's history, for two\nunrelated reasons.",
                "summary": "Delete progression check",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Progression not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/submissions/{submissionId}/check-reminders": {
            "get": {
                "operationId": "submissionCheckReminder.index",
                "description": "Every reminder scheduled for one submission, soonest-due first.\nNo existence check on the submission itself \u2014 an invalid or\nalready-deleted `submissionId` returns an empty list rather\nthan a 404, the same \"absence of matches vs. absence of parent\"\nambiguity flagged in\n`Api\\CampaignSubmissionController::getCampaignSubmissions()`.",
                "summary": "Get check reminders",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "submissionId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "operationId": "submissionCheckReminder.store",
                "description": "`due_at` must be a future date/time (`after:now`) \u2014 there is no\ncorresponding check that `$submissionId` actually exists, so a\nreminder can be created pointing at a submission ID that\ndoesn't exist (or existed and was since deleted), the same gap\nas `index()` above but in the write direction: this would\nsilently create an orphaned reminder rather than rejecting the\nrequest.\n\n`is_final_check` distinguishes a routine check-in from what's\npresumably the *last* scheduled check for a submission \u2014 that\ndistinction isn't acted on anywhere in this method itself\n(it's stored, not branched on), so whatever significance \"this\n     * is the final check\" carries (e.g. auto-closing the submission\nif it passes, escalating differently if it fails) lives outside\nthis controller, if it exists at all.",
                "summary": "Create check reminder",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "submissionId",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "due_at": {
                                        "type": "string",
                                        "format": "date-time"
                                    },
                                    "is_final_check": {
                                        "type": "boolean"
                                    }
                                },
                                "required": [
                                    "due_at"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/billboard/check-reminders/{id}/complete": {
            "put": {
                "operationId": "submissionCheckReminder.complete",
                "description": "Sets `completed` to `true` with no record of *what* the check\nfound \u2014 there's no field here for an outcome (e.g. \"post still\n     * live\" vs. \"post was taken down\"), no admin note, no timestamp\nfield referenced beyond whatever `updated_at` Eloquent maintains\nautomatically. If a failed check (e.g. discovering the\ninfluencer deleted their post) needs to trigger any\nconsequence \u2014 flagging the submission, notifying the brand \u2014\nnothing in this method does that; completing a reminder here\nonly ever records that the check *happened*, not what it\nrevealed. Can also be called repeatedly on an\nalready-`completed` reminder with no guard, same pattern as the\nmissing terminal-state checks flagged in\n`Api\\CampaignSubmissionController`'s approve/reject methods.",
                "summary": "Complete reminder",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Reminder not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/check-reminders/{id}": {
            "delete": {
                "operationId": "submissionCheckReminder.destroy",
                "description": "A straightforward hard delete with no special handling for\n`is_final_check` reminders specifically \u2014 deleting the one\nreminder marked as the final check for a submission removes it\nthe same as any routine one, with nothing in this method\nre-flagging another reminder as the new \"final\" one if that\ndistinction matters to whatever process relies on it existing.",
                "summary": "Delete reminder",
                "tags": [
                    "Campaign Submissions"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Reminder not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/countries": {
            "get": {
                "operationId": "location.countries",
                "description": "Returns every country-level `Location` row with its full\ndescendant tree eagerly loaded via `childrenRecursive` \u2014 states/\nregions, then cities, nested in a single response rather than\nrequiring a separate `children()` call per level. This is the\nheaviest endpoint in the controller: for a fully-populated\nlocation table, `childrenRecursive` walks every level of the\nhierarchy for every country in one query tree, so payload size\ngrows with the depth and breadth of the data, not just the\nnumber of countries. There is no pagination here \u2014 adding more\ncountries or deepening the hierarchy directly enlarges every\nresponse.\n\nNo filtering is applied beyond `type = 'country'`, so this\nalways returns the complete set; there's no way to request a\nsingle country's full tree without first finding its ID and\ncalling this anyway (`children()` returns one node's subtree,\nbut only if you already know that node's ID).",
                "summary": "Get all countries",
                "tags": [
                    "Location Management"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/location/{id}/children": {
            "get": {
                "operationId": "location.children",
                "description": "Returns a single location's full descendant subtree, keyed by\nits own ID rather than by type \u2014 the same endpoint serves a\ncountry's states, a state's cities, or any other level, since\n`Location` appears to be a self-referencing tree rather than\nseparate models per level. The `id` is not validated against\n`type` in any way, so this will just as readily return the\nsubtree of a city (likely empty, if cities are leaf nodes) as\na country.\n\nLike `countries()`, the full recursive tree is loaded in one\ncall with no depth limit \u2014 requesting a country here returns\nexactly as much data as `countries()` would for that one\ncountry, just without the other countries alongside it.",
                "summary": "Get location children",
                "tags": [
                    "Location Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Location not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/locations/search": {
            "get": {
                "operationId": "location.search",
                "description": "Free-text search across every location type at once \u2014 a query\nfor \"York\" would return New York the city, Yorkshire the\nregion, and any country containing those letters, all in one\nresult set ordered by `type` rather than by relevance or name.\nThe `LIKE '%...%'` pattern means this cannot use a standard\nindex, so search cost scales with the total number of location\nrows in the table, not just the matches \u2014 on a large location\ntable this is the most expensive read path in the controller\nper request, despite returning the smallest payloads.\n\nUnlike `countries()` and `children()`, results here do **not**\ninclude `childrenRecursive` \u2014 a search match returns just the\nmatched row, with no nested subtree, so a caller who searches\nand gets a country back still needs a separate `children()`\ncall to see its states and cities.\n\nThe `count` field reflects the full result set size, not a\npage size \u2014 there is no pagination or result limit applied, so\na broad query (e.g. a single common letter) can return every\nlocation in the system that matches.",
                "summary": "Search locations",
                "tags": [
                    "Location Management"
                ],
                "parameters": [
                    {
                        "name": "query",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "minLength": 1
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/billboard/locations": {
            "get": {
                "operationId": "locationApi.index",
                "description": "Top-level locations (`parent_id IS NULL`) with their full\nrecursive subtree loaded \u2014 the admin equivalent of\n`v1\\LocationController::countries()`, same unpaginated,\nfull-depth-tree cost profile flagged there.",
                "summary": "Get all locations",
                "tags": [
                    "Location Management"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "operationId": "locationApi.store",
                "description": "Creates one location, optionally with a nested tree of children\nin the same request via the recursive `createChildren()` helper\nbelow \u2014 a request can describe several levels of hierarchy\n(e.g. a state with cities, each city with wards) in a single\ncall rather than one `store()` per node.\n\n`type` is restricted to `country`, `state`, `city`, or `ward` \u2014\nnote this is a fixed four-level enum; nothing in the schema\nenforces that a `ward`'s parent is actually a `city`, a `city`'s\nparent is a `state`, etc. \u2014 `parent_id` only needs to reference\n*some* existing location, of any type, so a `ward` could be\ncreated as a direct child of a `country` with no\nintermediate `state`/`city`, and this method would accept it.\n\nEach child object in the optional `children` array is validated\nonly at the top level (`children` itself must be an array) \u2014\nthe individual child objects' `name`/`type` fields are accessed\ndirectly in `createChildren()` with no validation rules applied\nto them, the same unguarded-array-access pattern flagged in\n`Api\\CampaignApiController::createCampaignDeliverables()`; a\nmalformed child element missing `name` or `type` would throw\nrather than return a clean 422.",
                "summary": "Create location",
                "tags": [
                    "Location Management"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "name": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "type": {
                                        "type": "string",
                                        "enum": [
                                            "country",
                                            "state",
                                            "city",
                                            "ward"
                                        ]
                                    },
                                    "country_code": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 10
                                    },
                                    "flag": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "parent_id": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ]
                                    },
                                    "children": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    }
                                },
                                "required": [
                                    "name",
                                    "type"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/billboard/locations/{id}": {
            "get": {
                "operationId": "locationApi.show",
                "description": "Equivalent to `v1\\LocationController::children()`, same\ntype-agnostic, full-recursive-tree behavior \u2014 the only\ndifference between the two methods is the resource class used\n(`LocationResource` here vs. raw model array there) and which\nnamespace's docs group they fall under.",
                "summary": "Get specific location",
                "tags": [
                    "Location Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Location not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            },
            "put": {
                "operationId": "locationApi.update_1",
                "description": "Updates the location's own fields normally, but handles\n`children` very differently from `store()`: if `children` is\npresent in the request at all, **every existing child of this\nlocation is deleted first**, then the provided `children` array\nis recreated from scratch via the same `createChildren()`\nhelper. This is a destructive replace, not a merge \u2014 submitting\nan `update()` with a `children` array containing only *some* of\nthe location's existing children (e.g. a partial edit that\nintended to add one city without resending the other nine)\nwould delete the nine that weren't included. There's also no\nindication of whether `children()->delete()` cascades to\ngrandchildren (a deleted city's own wards) or merely detaches\none level \u2014 that depends on the `Location` model's relationship\nand any DB-level cascade configuration, not visible from this\nmethod.\n\nSending an update with **no** `children` key at all (vs. an\nempty `children: []`) leaves existing children untouched in\neither case `$request->has('children')` is false for a missing\nkey \u2014 but an explicit `children: []` *would* trigger the\ndelete-everything path with nothing to recreate, since `has()`\nchecks presence, not emptiness. That's a meaningfully different\noutcome from two payloads that might look interchangeable to a\ncaller who isn't aware of the distinction.",
                "summary": "Update location",
                "tags": [
                    "Location Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "name": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "type": {
                                        "type": "string",
                                        "enum": [
                                            "country",
                                            "state",
                                            "city",
                                            "ward"
                                        ]
                                    },
                                    "country_code": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 10
                                    },
                                    "flag": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "parent_id": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ]
                                    },
                                    "children": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Location not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            },
            "patch": {
                "operationId": "locationApi.update_2",
                "description": "Updates the location's own fields normally, but handles\n`children` very differently from `store()`: if `children` is\npresent in the request at all, **every existing child of this\nlocation is deleted first**, then the provided `children` array\nis recreated from scratch via the same `createChildren()`\nhelper. This is a destructive replace, not a merge \u2014 submitting\nan `update()` with a `children` array containing only *some* of\nthe location's existing children (e.g. a partial edit that\nintended to add one city without resending the other nine)\nwould delete the nine that weren't included. There's also no\nindication of whether `children()->delete()` cascades to\ngrandchildren (a deleted city's own wards) or merely detaches\none level \u2014 that depends on the `Location` model's relationship\nand any DB-level cascade configuration, not visible from this\nmethod.\n\nSending an update with **no** `children` key at all (vs. an\nempty `children: []`) leaves existing children untouched in\neither case `$request->has('children')` is false for a missing\nkey \u2014 but an explicit `children: []` *would* trigger the\ndelete-everything path with nothing to recreate, since `has()`\nchecks presence, not emptiness. That's a meaningfully different\noutcome from two payloads that might look interchangeable to a\ncaller who isn't aware of the distinction.",
                "summary": "Update location",
                "tags": [
                    "Location Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "name": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "type": {
                                        "type": "string",
                                        "enum": [
                                            "country",
                                            "state",
                                            "city",
                                            "ward"
                                        ]
                                    },
                                    "country_code": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 10
                                    },
                                    "flag": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "parent_id": {
                                        "type": [
                                            "integer",
                                            "null"
                                        ]
                                    },
                                    "children": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "items": {
                                            "type": "string"
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Location not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            },
            "delete": {
                "operationId": "locationApi.destroy",
                "description": "Deletes the location row itself. The method summary says this\n\"removes a location and all its children,\" but the code is a\nsingle `$location->delete()` call with **no explicit recursive\ndeletion of children visible in this method** \u2014 whether\nchildren actually get removed depends entirely on whether the\n`Location` model defines a cascading delete (e.g. a model event\ndeleting `childrenRecursive`, or a DB foreign key with\n`ON DELETE CASCADE`) somewhere outside this controller. If no\nsuch cascade exists, deleting a country with states and cities\nunderneath it would leave those children as orphaned rows\npointing at a `parent_id` that no longer exists, while still\nappearing in `index()`/`show()`/`search()` for any *other*\nlookup that doesn't route through the deleted parent. Worth\nconfirming the cascade exists before trusting this method's own\nstated behavior.\n\nAlso worth noting: deleting a location with no check for\nexisting references elsewhere in the system (e.g. a `Brand` or\n`User` row storing this location's ID directly, if any do) could\nleave those references dangling \u2014 there's no foreign-key\nconstraint check performed in this method, and whether one\nexists at the database level isn't visible from here.",
                "summary": "Delete location",
                "tags": [
                    "Location Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Location not found"
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/locations/search": {
            "get": {
                "operationId": "locationApi.search",
                "description": "The admin version of `v1\\LocationController::search()`, with\none functional difference: this version **does** eager-load\n`childrenRecursive` on every result, where the v1 version\ndeliberately does not. A search hit here returns its full\nsubtree immediately; the same search in v1 returns just the\nmatched row, requiring a follow-up `children()`/`show()` call\nto see further down the tree. Same `LIKE '%...%'` cost profile\nas v1's version otherwise.",
                "summary": "Search locations",
                "tags": [
                    "Location Management"
                ],
                "parameters": [
                    {
                        "name": "query",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "minLength": 1
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/mpesa/mpesa-deposit": {
            "post": {
                "operationId": "mpesa.deposit",
                "description": "Sends an STK push prompt to the user's phone and creates a\n`pending` credit `Ledger` entry keyed on the `CheckoutRequestID`\nreturned by Safaricom \u2014 this row exists purely as a placeholder\nthe eventual `callback()` can match against; the deposit is not\nconsidered to have happened, and the user's derived balance does\nnot change, until that callback arrives and flips the status to\n`successful`.\n\nIf the user never completes the prompt on their phone, this\n`pending` row is left behind indefinitely \u2014 nothing in this\nmethod or `callback()` expires it, so a abandoned STK Push\nleaves a permanently pending ledger entry unless something\nelse in the system reconciles or times it out.\n\nThe amount is taken directly from the request and is not\nre-validated against what M-Pesa actually reports back in the\ncallback \u2014 see `callback()` for what happens if those two\nnumbers disagree.",
                "summary": "Initiate deposit via M-Pesa STK Push",
                "tags": [
                    "M-Pesa Payments"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "phone": {
                                        "type": "string"
                                    },
                                    "amount": {
                                        "type": "number",
                                        "minimum": 1
                                    }
                                },
                                "required": [
                                    "phone",
                                    "amount"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "Failed to initiate payment."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/mpesa/mpesa-withdraw": {
            "post": {
                "operationId": "mpesa.withdraw",
                "description": "The most involved method in this controller \u2014 validates tariff\neligibility, checks for duplicate requests, locks and recomputes\nthe user's balance, writes two ledger rows, and only then calls\nout to M-Pesa, all before any money actually leaves the\nplatform's side.\n\n**Tariff selection** determines the withdrawal fee: the request's\nminimum allowed amount is the lowest `min_amount` across *all*\nactive tariffs, but the actual fee charged comes from whichever\nsingle tariff band the requested amount falls into\n(`min_amount <= amount <= max_amount`). If active tariff bands\nhave a gap between them, an amount that satisfies the overall\nminimum but falls in that gap is rejected with \"exceeds allowed\n     * tariff limits\" \u2014 a potentially confusing message for an amount\nthat looks like it should be withdrawable.\n\n**Duplicate detection** is a heuristic, not a true idempotency\nkey: it matches on same user, same exact amount, status still\n`processing` or `pending`, created within the last minute. A\nlegitimate second withdrawal of a *different* amount, or the\nsame amount more than 60 seconds later, is not caught by this\ncheck at all \u2014 it only protects against rapid, identical\ndouble-submissions (e.g. a double-tapped button).\n\n**Balance computation** happens inside `lockForUpdate()`, so\nconcurrent withdrawal requests from the same user are\nserialized at the database level rather than racing \u2014 but the\nbalance is recomputed from the *entire* ledger history on every\nsingle withdrawal call, which means this cost grows with the\nuser's total transaction count over time rather than staying\nconstant. Balance counts `successful`/`completed` credits, but\ntreats `processing`/`pending` **debits and fees** as already\nreducing the available balance \u2014 so funds are reserved\noptimistically the moment a withdrawal starts processing, before\nM-Pesa confirms it.\n\n**Ledger writes happen before the M-Pesa call**, inside the same\ntransaction, but the call to\n`$this->mpesaService->withdrawalRequest()` happens *after* the\ntransaction commits \u2014 if that call fails or times out, the\n`processing` debit and `completed` fee rows already exist and\nare not rolled back; the withdrawal appears reserved in the\nuser's balance with no corresponding B2C request having\nsucceeded, until `b2cResult()` (or some other reconciliation)\neventually resolves it.\n\nAll validation failures in this method return a generic error\nwith no explicit status code, which (depending on\n`ApiHelper::error()`'s default) likely differs from `deposit()`'s\nexplicit `401`/`500` codes for analogous failures elsewhere in\nthis controller.",
                "summary": "Withdraw funds via M-Pesa",
                "tags": [
                    "M-Pesa Payments"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "phone": {
                                        "type": "string"
                                    },
                                    "amount": {
                                        "type": "number",
                                        "minimum": 10
                                    }
                                },
                                "required": [
                                    "phone",
                                    "amount"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "400": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "anyOf": [
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "Withdrawal failed"
                                                },
                                                "errors": {
                                                    "type": "object",
                                                    "properties": {
                                                        "mpesa": {
                                                            "type": "string"
                                                        }
                                                    },
                                                    "required": [
                                                        "mpesa"
                                                    ]
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "Invalid phone number format."
                                                },
                                                "errors": {
                                                    "type": "string"
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "Try again later."
                                                },
                                                "errors": {
                                                    "type": "string"
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string"
                                                },
                                                "errors": {
                                                    "type": "string"
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "Duplicate withdrawal detected. Try again shortly."
                                                },
                                                "errors": {
                                                    "type": "string"
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "Withdrawal amount exceeds allowed tariff limits."
                                                },
                                                "errors": {
                                                    "type": "string"
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "We are experiencing technical issues. Please try again later."
                                                },
                                                "errors": {
                                                    "type": "string"
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/mpesa/callback": {
            "post": {
                "operationId": "mpesa.callback",
                "description": "Always returns `{ResultCode: 0, ResultDesc: 'Accepted'}` to\nSafaricom regardless of what actually happened on this end \u2014\nthat response only acknowledges receipt of the callback and\ntells Safaricom not to retry; it carries no information about\nwhether the deposit was actually credited.\n\nMatches the callback to a ledger row purely by\n`CheckoutRequestID` \u2014 if no matching `pending` row exists (e.g.\nit was already processed by an earlier, duplicate callback, or\nnever existed), the callback is silently accepted and does\nnothing further. Safaricom can and does resend callbacks, so\nthis method has no explicit idempotency guard beyond that\nlookup; if it runs twice for the same successful deposit before\nthe first call finishes updating the row, both calls could\nattempt the same update concurrently.\n\nOn success (`ResultCode == 0`), the ledger entry's `reference`\nis **overwritten** from the `CheckoutRequestID` to the M-Pesa\nreceipt number \u2014 meaning the original checkout ID used to look\nthis row up is discarded after the first successful match; a\nsecond callback for the same `CheckoutRequestID` (a known\nduplicate-delivery scenario with M-Pesa) would no longer find\nthis row at all, since the lookup is always by `reference` and\nthe reference has already changed.\n\nCrucially, **the amount confirmed by M-Pesa\n(`CallbackMetadata.Amount`) is read but never written back to\nthe ledger** \u2014 if it differs from the amount the user originally\nrequested in `deposit()`, the ledger entry keeps the\n*original* requested amount, not the amount M-Pesa actually\nprocessed. The `$amount` variable here is effectively unused\nfor anything except a same-value fallback.",
                "summary": "M-Pesa STK Push callback",
                "tags": [
                    "M-Pesa Payments"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "Body": {
                                        "type": "object",
                                        "properties": {
                                            "stkCallback": {
                                                "type": "string"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "ResultCode": {
                                            "type": "integer"
                                        },
                                        "ResultDesc": {
                                            "type": "string",
                                            "const": "Accepted"
                                        }
                                    },
                                    "required": [
                                        "ResultCode",
                                        "ResultDesc"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/mpesa/withdrawal/callback": {
            "post": {
                "operationId": "mpesa.b2cResult",
                "description": "Resolves a `processing` debit entry to `successful` or `failed`\nonce Safaricom confirms the outcome of the B2C payout. Matches\nby `OriginatorConversationID`, which must exactly equal the\n`reference` UUID generated in `withdraw()` \u2014 note this is a\n**different** identifier than the `CheckoutRequestID` used to\nmatch deposits in `callback()`, since deposits (STK Push) and\nwithdrawals (B2C) are separate M-Pesa APIs with separate\nidentifier schemes.\n\nThe associated `fee` ledger row (created alongside the debit in\n`withdraw()`, sharing the same reference with a `-FEE` suffix)\nis never looked up or updated here \u2014 it was created as\n`completed` immediately and stays that way regardless of\nwhether the withdrawal itself ultimately succeeds or fails. A\nfailed withdrawal therefore leaves the fee charged and marked\ncomplete while the principal debit is marked `failed` \u2014 whether\nthe fee is refunded in that case happens elsewhere, if at all,\nsince nothing in this method addresses it.\n\nLogs the entire raw request body at `info` level before any\nvalidation \u2014 this includes phone numbers and any other\nPII Safaricom includes in the payload, written to logs\nunconditionally on every callback received.",
                "summary": "M-Pesa B2C result callback",
                "tags": [
                    "M-Pesa Payments"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "Result": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "ResultCode": {
                                            "type": "integer"
                                        },
                                        "ResultDesc": {
                                            "type": "string",
                                            "const": "Accepted"
                                        }
                                    },
                                    "required": [
                                        "ResultCode",
                                        "ResultDesc"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/notifications": {
            "get": {
                "operationId": "notification.index",
                "description": "Returns the full notification feed for the authenticated user\nvia `getUserNotifications()`, with no pagination, status filter\n(read/unread), or type filter applied at this layer \u2014 if any of\nthat exists, it's inside the service and not visible from this\nmethod's signature. For a long-lived account that's never had\nits notifications pruned, this returns every notification ever\ngenerated for that user in one response, unlike `counts()`\nbelow, which is a cheaper, summary-only alternative for cases\nwhere the full feed isn't needed (e.g. a badge count in a nav\nbar).\n\nUnlike `markAsRead()`, this method has no explicit ownership\ncheck written in the controller \u2014 it relies entirely on\n`getUserNotifications($user)` internally filtering by the\npassed-in user, since the method takes the resolved `$user`\nobject directly rather than a route-bound ID that could be\ntampered with.",
                "summary": "Get user notifications",
                "tags": [
                    "Notifications"
                ],
                "responses": {
                    "200": {
                        "description": "Array of `NotificationResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/NotificationResource"
                                            }
                                        }
                                    },
                                    "required": [
                                        "data"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/notifications/counts": {
            "get": {
                "operationId": "notification.counts",
                "description": "Returns unread counts grouped by notification type (e.g. how\nmany unread \"application approved\" vs \"payment received\"\nnotifications exist), intended for places that need a quick\nsummary \u2014 badges, tabs, a notification bell \u2014 without paying\nthe cost of loading and serializing every notification via\n`index()`.\n\nBypasses `NotificationResource` entirely and returns the\nservice's array shape directly through `response()->json()`,\nunlike every other method in this controller \u2014 so this\nendpoint's response structure is defined by\n`getUnreadCounts()`'s return value alone, not by a resource\nclass, and would need to be kept in sync manually if the\nnotification type list ever changes.",
                "summary": "Get notification counts",
                "tags": [
                    "Notifications"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "array",
                                    "items": {}
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/notifications/{notification}/read": {
            "patch": {
                "operationId": "notification.markAsRead",
                "description": "The only method in this controller with an explicit,\nin-controller ownership check rather than one delegated to the\nservice: `$notification` is route-model-bound by ID alone, so\nwithout the `user_id !== ...` comparison here, any\nauthenticated user could mark \u2014 and by extension, probably\nalso read the contents of \u2014 another user's notification just by\nguessing or incrementing its ID. This is a plain equality check\nrather than a Laravel policy (`$this->authorize(...)`), which is\na different authorization pattern than `CampaignController` and\n`CampaignApplicationController` use elsewhere in this API.\n\nMarking as read here only changes a status field \u2014 `markAsRead()`\ndoes not delete the notification, so it remains visible in a\nlater `index()` call, just no longer counted in `counts()`.\nThe endpoint is not idempotent in its response shape: calling\nit again on an already-read notification is not guarded against\nin this method, so behavior on a second call depends on whether\n`NotificationService::markAsRead()` treats re-marking an\nalready-read notification as a no-op or an error.",
                "summary": "Mark notification as read",
                "tags": [
                    "Notifications"
                ],
                "parameters": [
                    {
                        "name": "notification",
                        "in": "path",
                        "required": true,
                        "description": "The notification ID",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "`NotificationResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/NotificationResource"
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "message": {
                                            "type": "string",
                                            "const": "Unauthorized"
                                        }
                                    },
                                    "required": [
                                        "message"
                                    ]
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/recruitment/roles": {
            "get": {
                "operationId": "recruitment.getRoles",
                "description": "Returns the list of open roles a candidate can apply for \u2014\nintended to populate the `role` dropdown on the application\nform before submission, so the frontend doesn't hardcode role\nnames that may change independently of this controller. The\n`role` value submitted in `store()` is validated only as a\ngeneric string, not against this list, so there's nothing\nhere enforcing that a submitted application actually names a\nrole this endpoint currently returns \u2014 a stale frontend, or a\ndirect API call, could submit an application for a role that\nno longer exists or never did.\n\nUnlike the rest of this API, the response here is a plain\narray wrapped in `response()->json()` rather than a Resource\nclass, so its shape is defined entirely by whatever\n`RecruitmentService::getRoles()` returns.",
                "summary": "Get available roles",
                "tags": [
                    "Recruitment"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "roles": {
                                            "type": "string"
                                        }
                                    },
                                    "required": [
                                        "roles"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/recruitment": {
            "post": {
                "operationId": "recruitment.store",
                "description": "Accepts a single application form that covers several distinct\nrole types at once \u2014 product management, engineering\n(Laravel/React), advisory \u2014 through one shared set of optional\nfields, rather than separate endpoints or separate validation\nrule sets per role. Every role-specific field\n(`years_product_management`, `years_laravel`, `years_react`,\n`payment_integration`, `years_advisory`, `startups_scaled`,\n`investor_network_size`) is `nullable`, so nothing here\nenforces that a candidate applying for, say, an engineering\nrole actually fills in `years_laravel` and `years_react` rather\nthan the advisory-specific fields, or none of them at all. Any\nsuch role-appropriate field requirement, if it exists, lives\ninside `RecruitmentService::store()`, not in this validation\nblock.\n\nRequest keys are camelCase (matching the frontend form) and are\nexplicitly remapped to snake_case before being passed to the\nservice \u2014 `fullName` becomes `full_name`, `whyJoin` becomes\n`why_join`, and so on \u2014 so the validated array and the array\nactually passed to the service are deliberately different\nshapes; a new field added to validation here also needs a\nmatching line added to the `$data` mapping below it, or it will\nbe validated but silently dropped before reaching the service.\n\n`resume` is a required single file (PDF/DOC/DOCX, max 10MB);\n`portfolioFile` is a separate, optional array of up to 5 files\nwith a wider set of allowed types (adds JPG/PNG) and a smaller\nper-file limit (5MB). Both `resume` and the entries in\n`portfolio_files` are passed into `$data` as raw uploaded file\nobjects, not as stored paths or URLs \u2014 unlike every other\nfile-handling controller in this API (`CampaignController`,\n`CampaignSubmissionController`), there is no `$file->store(...)`\ncall in this method, so persisting the uploaded files to disk\nand recording their paths is entirely\n`RecruitmentService::store()`'s responsibility.\n\nA candidate can additionally supply `portfolioUrl` alongside or\ninstead of uploaded portfolio files \u2014 both are optional and\nindependent, so an application can have neither, either, or\nboth.",
                "summary": "Submit a recruitment application",
                "tags": [
                    "Recruitment"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "fullName": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "email": {
                                        "type": "string",
                                        "format": "email",
                                        "maxLength": 255
                                    },
                                    "phone": {
                                        "type": "string",
                                        "maxLength": 20
                                    },
                                    "location": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "role": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "whyJoin": {
                                        "type": "string"
                                    },
                                    "resume": {
                                        "type": "string",
                                        "format": "binary",
                                        "contentMediaType": "application/octet-stream",
                                        "maxLength": 10240
                                    },
                                    "portfolioUrl": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "uri",
                                        "description": "5MB each",
                                        "maxLength": 255
                                    },
                                    "yearsProductManagement": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 50
                                    },
                                    "productDescription": {
                                        "type": [
                                            "string",
                                            "null"
                                        ]
                                    },
                                    "linkedin": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "uri",
                                        "maxLength": 255
                                    },
                                    "github": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "format": "uri",
                                        "maxLength": 255
                                    },
                                    "yearsLaravel": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 50
                                    },
                                    "yearsReact": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 50
                                    },
                                    "paymentIntegration": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 255
                                    },
                                    "yearsAdvisory": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 50
                                    },
                                    "startupsScaled": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 50
                                    },
                                    "investorNetworkSize": {
                                        "type": [
                                            "string",
                                            "null"
                                        ],
                                        "maxLength": 50
                                    },
                                    "portfolioFile": {
                                        "type": [
                                            "array",
                                            "null"
                                        ],
                                        "description": "10MB",
                                        "items": {
                                            "type": "string",
                                            "format": "binary",
                                            "contentMediaType": "application/octet-stream",
                                            "maxLength": 5120
                                        },
                                        "maxItems": 5
                                    }
                                },
                                "required": [
                                    "fullName",
                                    "email",
                                    "phone",
                                    "role",
                                    "whyJoin",
                                    "resume"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "`RecruitmentResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/RecruitmentResource"
                                        }
                                    },
                                    "required": [
                                        "data"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            }
        },
        "/auth/{provider}/redirect": {
            "get": {
                "operationId": "socialAuth.redirect",
                "description": "Sends the browser to Google's or Apple's own OAuth consent\nscreen via Socialite, using `stateless()` mode \u2014 meaning no\nserver-side session is used to track the request, and\nconsequently no CSRF `state` parameter is verified on return.\nThat's a deliberate tradeoff for an API-driven mobile/SPA flow\nwhere a shared session between `redirect()` and\n`handleProviderCallback()` can't be relied on, but it's a\ndifferent security posture than `TiktokAuthController::redirect()`,\nwhich manually constructs and verifies its own encrypted,\ntimestamped `state` payload precisely because TikTok isn't\nhandled by Socialite and so doesn't get this stateless\nconvenience for free \u2014 there is no equivalent replay/CSRF\nprotection here for Google or Apple.\n\n`$provider` is restricted to `google` and `apple` via a hardcoded\narray check; anything else returns a plain 404 rather than a\nstructured API error, since this is a redirect-based browser\nflow rather than a JSON endpoint.",
                "summary": "Redirect to social provider",
                "tags": [
                    "Social Authentication"
                ],
                "parameters": [
                    {
                        "name": "provider",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    }
                },
                "security": []
            }
        },
        "/auth/{provider}/callback": {
            "get": {
                "operationId": "socialAuth.handleProviderCallback",
                "description": "Exchanges the OAuth code for the provider's profile via\nSocialite, then matches or creates a `User` by **email alone** \u2014\na meaningfully different identity strategy than\n`TiktokAuthController::callback()`, which matches by\n`tiktok_open_id` specifically because TikTok accounts may have no\nemail at all. A practical consequence here: if a user previously\nregistered with email/password (or via the other social\nprovider) using the same email address, `firstOrCreate()` finds\nthat existing row and logs into it rather than creating a\nsecond account \u2014 so Google and Apple sign-in are implicitly\nthe same account for any user with a matching email, by design\nor by accident of this lookup.\n\nThe `google_photo` field is populated from `getAvatar()`\nregardless of which provider was actually used \u2014 there's no\n`apple_photo` column or per-provider branching here, so an\nApple sign-in's avatar (when Apple supplies one) is stored\nunder a field named for Google. This only fires on the\n*first* sign-in for a given email, since `firstOrCreate()`'s\nsecond argument is only used when creating a new row \u2014 a\nreturning user's `full_name`, photo, or other profile fields\nare never refreshed from the provider on subsequent logins,\neven if their name or photo changed on Google/Apple's side\nsince the account was created.\n\n`user_name` is derived by truncating the email at `@` \u2014 with no\nuniqueness check or collision handling, so two different users\nsigning in with `same.prefix@gmail.com` and\n`same.prefix@yahoo.com` would both be assigned the username\n`same.prefix`, unless a database-level unique constraint exists\nand throws at insert time (which this method does not appear to\ncatch).\n\n`email_verified_at` is set to `now()` unconditionally on\ncreation, on the basis that the identity provider itself\nvouches for the email \u2014 there's no separate verification step\nfor socially-created accounts, unlike whatever flow exists for\nemail/password registration (e.g. the OTP-based flow seen in\nother controllers in this API).\n\nOn any Socialite exception (expired/invalid code, network\nfailure, provider error), the user is redirected back to the\nfrontend login page with a generic `error` query parameter \u2014\nthe original exception message is not surfaced or logged here,\nso diagnosing *why* a given social login failed requires\nreproducing it rather than reading this controller's output.",
                "summary": "Handle social provider callback",
                "tags": [
                    "Social Authentication"
                ],
                "parameters": [
                    {
                        "name": "provider",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    }
                },
                "security": []
            }
        },
        "/support": {
            "post": {
                "operationId": "support.store",
                "description": "Allows an authenticated user to submit a feedback or issue ticket.",
                "summary": "Submit a support ticket",
                "tags": [
                    "Support"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "type": {
                                        "type": "string",
                                        "enum": [
                                            "feedback",
                                            "issue"
                                        ]
                                    },
                                    "category": {
                                        "type": "string",
                                        "enum": [
                                            "billing",
                                            "campaign",
                                            "account",
                                            "general",
                                            "technical",
                                            "partnerships"
                                        ]
                                    },
                                    "subject": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "description": {
                                        "type": "string",
                                        "maxLength": 5000
                                    },
                                    "priority": {
                                        "type": "string",
                                        "enum": [
                                            "low",
                                            "medium",
                                            "high"
                                        ]
                                    }
                                },
                                "required": [
                                    "type",
                                    "subject",
                                    "description"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            },
            "get": {
                "operationId": "support.index",
                "description": "Returns a paginated list of support tickets for the authenticated user.",
                "summary": "List own tickets",
                "tags": [
                    "Support"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/support/{ticket}": {
            "get": {
                "operationId": "support.show",
                "description": "Returns a ticket with its full response thread.",
                "summary": "View a single ticket",
                "tags": [
                    "Support"
                ],
                "parameters": [
                    {
                        "name": "ticket",
                        "in": "path",
                        "required": true,
                        "description": "The ticket ID",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/support/{ticket}/reply": {
            "post": {
                "operationId": "support.reply",
                "description": "Allows the authenticated user to add a reply to their own ticket.",
                "summary": "Reply to a ticket",
                "tags": [
                    "Support"
                ],
                "parameters": [
                    {
                        "name": "ticket",
                        "in": "path",
                        "required": true,
                        "description": "The ticket ID",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "message": {
                                        "type": "string",
                                        "maxLength": 5000
                                    }
                                },
                                "required": [
                                    "message"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/billboard/support": {
            "get": {
                "operationId": "supportApi.index",
                "description": "Returns a paginated list of all support tickets, filterable by status, type, priority, and category.",
                "summary": "List all tickets",
                "tags": [
                    "Support Management"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/support/{ticket}": {
            "get": {
                "operationId": "supportApi.show",
                "description": "Returns the full ticket details along with the response thread.",
                "summary": "View a single ticket",
                "tags": [
                    "Support Management"
                ],
                "parameters": [
                    {
                        "name": "ticket",
                        "in": "path",
                        "required": true,
                        "description": "The ticket ID",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    }
                }
            }
        },
        "/billboard/support/{ticket}/reply": {
            "post": {
                "operationId": "supportApi.reply",
                "description": "Allows an admin to respond to a support ticket.",
                "summary": "Reply to a ticket",
                "tags": [
                    "Support Management"
                ],
                "parameters": [
                    {
                        "name": "ticket",
                        "in": "path",
                        "required": true,
                        "description": "The ticket ID",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "message": {
                                        "type": "string",
                                        "maxLength": 5000
                                    },
                                    "admin_id": {
                                        "type": "integer"
                                    }
                                },
                                "required": [
                                    "message",
                                    "admin_id"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "201": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    }
                }
            }
        },
        "/billboard/support/{ticket}/status": {
            "patch": {
                "operationId": "supportApi.updateStatus",
                "description": "Changes the status of a support ticket (in_progress, resolved, closed).",
                "summary": "Update ticket status",
                "tags": [
                    "Support Management"
                ],
                "parameters": [
                    {
                        "name": "ticket",
                        "in": "path",
                        "required": true,
                        "description": "The ticket ID",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "status": {
                                        "type": "string",
                                        "enum": [
                                            "open",
                                            "in_progress",
                                            "resolved",
                                            "closed"
                                        ]
                                    },
                                    "admin_id": {
                                        "type": "integer"
                                    }
                                },
                                "required": [
                                    "status"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    }
                }
            }
        },
        "/billboard/support/{ticket}/assign": {
            "post": {
                "operationId": "supportApi.assign",
                "description": "Assigns a support ticket to a specific admin for handling.",
                "summary": "Assign ticket to an admin",
                "tags": [
                    "Support Management"
                ],
                "parameters": [
                    {
                        "name": "ticket",
                        "in": "path",
                        "required": true,
                        "description": "The ticket ID",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "admin_id": {
                                        "type": "integer"
                                    }
                                },
                                "required": [
                                    "admin_id"
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    }
                }
            }
        },
        "/auth/tiktok/redirect": {
            "get": {
                "operationId": "tiktokAuth.redirect",
                "description": "Builds TikTok's own `state` parameter manually, rather than\nrelying on Socialite's `stateless()` mode as\n`SocialAuthController` does for Google/Apple \u2014 necessary\nbecause TikTok has no first-party Socialite driver, but it also\nmeans CSRF protection here is hand-rolled rather than borrowed:\nthe state payload is a `nonce` plus a timestamp, JSON-encoded\nand symmetrically encrypted with the app key via `Crypt`, so it\ncan only be decrypted by this same application and `callback()`\ncan verify it hasn't expired (see below). The `nonce` itself is\ngenerated but never checked against anything on return \u2014 it\nadds entropy to the encrypted payload but is not stored\nserver-side and compared, so it doesn't provide single-use\nreplay protection by itself; the actual replay defense here is\nthe 5-minute expiry window enforced in `callback()`.\n\nScopes requested are fixed at `user.info.basic`,\n`user.info.profile`, and `user.info.stats` \u2014 note `stats`\nis what allows `tiktok_followers` to be populated in\n`callback()`; removing that scope would silently zero out\nfollower counts for new signups rather than fail visibly.",
                "summary": "Redirect user to TikTok OAuth",
                "tags": [
                    "TikTok Authentication"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    }
                },
                "security": []
            }
        },
        "/auth/tiktok/callback": {
            "get": {
                "operationId": "tiktokAuth.callback",
                "description": "The most involved auth flow in this API \u2014 four sequential\nsteps, each with its own failure path, all wrapped in one\ntry/catch for anything unexpected:\n\n1. **State validation.** The encrypted `state` from `redirect()`\n   is decrypted and checked for a `ts` no older than 5 minutes.\n   A `Crypt::decryptString()` failure (tampered or malformed\n   state) throws and is caught by the outer `catch (\\Throwable)`\n   below \u2014 it does **not** hit the \"Expired TikTok request\"\n   branch, so a tampered state and a genuinely expired one\n   surface to the user as different error messages\n   (\"Server error\" vs \"Expired TikTok request\") even though\n   both are really \"this state can't be trusted.\"\n2. **Code exchange.** The authorization `code` is exchanged for\n   an access token. A missing `access_token` or `open_id` in\n   the response is logged at `error` level (with TikTok's full\n   response body) and redirects with \"token exchange failed\" \u2014\n   note the full token response, which on a *successful*\n   exchange would include the live access token, is logged at\n   `info` level just above this check, regardless of outcome.\n3. **Profile fetch.** Uses the access token to pull\n   `open_id, username, avatar_url, follower_count`. Same\n   logging pattern as step 2 \u2014 the full profile response is\n   logged at `info` unconditionally before the success check.\n4. **User match/create.** See below.\n\n**Identity & data model.** Unlike `SocialAuthController`, which\nmatches by `email`, this matches strictly by `tiktok_open_id` \u2014\nbecause TikTok's API does not reliably provide an email address\nat all, this flow creates accounts with `email` and\n`email_verified_at` both explicitly forced to `null` (flagged\nwith an `// IMPORTANT` comment in the original source), even\nthough `User::firstOrCreate()`'s defaults array would otherwise\njust omit those keys. That null email has real downstream\nconsequences: password-reset-by-email, email-based notifications,\nand any future email/Google/Apple account-merging logic (see\n`SocialAuthController::handleProviderCallback()`, which matches\nby email) will not function for a TikTok-only account unless\nthe user separately adds and verifies an email later.\n\nAs with `SocialAuthController`, `firstOrCreate()`'s defaults\nonly apply on first creation \u2014 a *returning* TikTok user's\n`tiktok_username`, `tiktok_avatar`, and `tiktok_followers` are\nnever refreshed from the fresh profile data just fetched in\nstep 3, even though that data is sitting right there in\n`$tiktokUser` on every single login.\n\n**Error handling.** The outer `catch (\\Throwable $e)` logs the\nfull exception message and stack trace at `error` level and\nredirects with a generic \"Server error during TikTok login\" \u2014\nthis is the only one of the four failure paths that logs a\nstack trace; the other three log only the API response body,\nwith no trace of where in this method the failure was detected.",
                "summary": "Handle TikTok OAuth callback",
                "tags": [
                    "TikTok Authentication"
                ],
                "parameters": [
                    {
                        "name": "code",
                        "in": "query",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "state",
                        "in": "query",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    }
                },
                "security": []
            }
        },
        "/transactions": {
            "get": {
                "operationId": "transactions.index",
                "description": "The read-facing view of everything `MpesaController::withdraw()`\nand `deposit()` write into the `Ledger` table \u2014 `type` here\nmeans one of `credit`, `debit`, or `fee` (the same values\n`MpesaController` writes), and `status` means `pending`,\n`processing`, `successful`, `completed`, or `failed`. Because\na single withdrawal writes two separate `Ledger` rows (the\n`debit` and its paired `fee`, linked only by a shared reference\nwith a `-FEE` suffix), this endpoint returns them as two\nindependent line items rather than one combined \"withdrawal\"\ntransaction \u2014 a caller wanting a single withdrawal's total\ncost has to find both rows and add them together client-side.\n\n`reference` filtering uses `LIKE '%...%'`, the same unanchored\npattern flagged in `LocationController::search()` \u2014 matching\nthe `-FEE` suffix from a withdrawal's fee row this way is one\nconcrete reason that pattern is used here rather than an exact\nmatch: a search for the withdrawal's base UUID alone would\notherwise miss its paired fee entry.\n\n`per_page` is caller-controlled with a default of 20 and no\nvisible upper bound enforced in this method \u2014 unlike\n`BrandController::index()`, which hardcodes a fixed page size\nwith no override at all, this endpoint trusts the query\nparameter directly, so a very large `per_page` value is only\nas bounded as whatever Laravel's paginator or a global request\nlimit enforces elsewhere, not by this controller.\n\nFiltering by `type`/`status`/`reference` is always scoped to\n`where('user_id', $user->id)` first \u2014 there's no path through\nthis method's query building that could leak another user's\nledger rows, since every filter is chained onto the\nalready-scoped query rather than applied independently.",
                "summary": "Get user transactions",
                "tags": [
                    "Transactions"
                ],
                "parameters": [
                    {
                        "name": "per_page",
                        "in": "query",
                        "schema": {
                            "type": "string",
                            "default": 20
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Paginated set of `TransactionResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "type": "array",
                                            "items": {
                                                "$ref": "#/components/schemas/TransactionResource"
                                            }
                                        },
                                        "links": {
                                            "type": "object",
                                            "properties": {
                                                "first": {
                                                    "type": [
                                                        "string",
                                                        "null"
                                                    ]
                                                },
                                                "last": {
                                                    "type": [
                                                        "string",
                                                        "null"
                                                    ]
                                                },
                                                "prev": {
                                                    "type": [
                                                        "string",
                                                        "null"
                                                    ]
                                                },
                                                "next": {
                                                    "type": [
                                                        "string",
                                                        "null"
                                                    ]
                                                }
                                            },
                                            "required": [
                                                "first",
                                                "last",
                                                "prev",
                                                "next"
                                            ]
                                        },
                                        "meta": {
                                            "type": "object",
                                            "properties": {
                                                "current_page": {
                                                    "type": "integer",
                                                    "minimum": 1
                                                },
                                                "from": {
                                                    "type": [
                                                        "integer",
                                                        "null"
                                                    ],
                                                    "minimum": 1
                                                },
                                                "last_page": {
                                                    "type": "integer",
                                                    "minimum": 1
                                                },
                                                "links": {
                                                    "type": "array",
                                                    "description": "Generated paginator links.",
                                                    "items": {
                                                        "type": "object",
                                                        "properties": {
                                                            "url": {
                                                                "type": [
                                                                    "string",
                                                                    "null"
                                                                ]
                                                            },
                                                            "label": {
                                                                "type": "string"
                                                            },
                                                            "active": {
                                                                "type": "boolean"
                                                            }
                                                        },
                                                        "required": [
                                                            "url",
                                                            "label",
                                                            "active"
                                                        ]
                                                    }
                                                },
                                                "path": {
                                                    "type": [
                                                        "string",
                                                        "null"
                                                    ],
                                                    "description": "Base path for paginator generated URLs."
                                                },
                                                "per_page": {
                                                    "type": "integer",
                                                    "description": "Number of items shown per page.",
                                                    "minimum": 0
                                                },
                                                "to": {
                                                    "type": [
                                                        "integer",
                                                        "null"
                                                    ],
                                                    "description": "Number of the last item in the slice.",
                                                    "minimum": 1
                                                },
                                                "total": {
                                                    "type": "integer",
                                                    "description": "Total number of items being paginated.",
                                                    "minimum": 0
                                                }
                                            },
                                            "required": [
                                                "current_page",
                                                "from",
                                                "last_page",
                                                "links",
                                                "path",
                                                "per_page",
                                                "to",
                                                "total"
                                            ]
                                        }
                                    },
                                    "required": [
                                        "data",
                                        "links",
                                        "meta"
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/transactions/{ledger}": {
            "get": {
                "operationId": "transactions.show",
                "description": "Returns a single `Ledger` row by route-bound ID with **no\nownership check at all** \u2014 `$ledger` is resolved purely from\nthe route parameter, and unlike `index()` immediately above it\n(which always scopes by `where('user_id', $user->id)`), nothing\nhere compares `$ledger->user_id` to `$request->user()->id`. The\n`Request $request` parameter is accepted but never used in\nthis method body. This is the same shape of gap flagged\nearlier in `CampaignSubmissionController::getSubmission()` \u2014\na bare ID lookup with no policy and no manual ownership\ncomparison \u2014 except here the exposed record is financial: a\nsequential or guessable `Ledger` ID would let an authenticated\nuser view another user's transaction amount, type, status, and\nreference (including, per `MpesaController::callback()`, the\nactual M-Pesa receipt number for completed deposits) by\nrequesting `/transactions/{id}` for IDs that aren't theirs.\n\nThis is worth verifying against route middleware or a\nmodel policy bound elsewhere (e.g. via route-level\n`can:view,ledger` middleware not shown in this controller)\nbefore treating it as confirmed \u2014 but nothing in this method\nitself enforces ownership.",
                "summary": "Get specific transaction",
                "tags": [
                    "Transactions"
                ],
                "parameters": [
                    {
                        "name": "ledger",
                        "in": "path",
                        "required": true,
                        "description": "The ledger ID",
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "`TransactionResource`",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "data": {
                                            "$ref": "#/components/schemas/TransactionResource"
                                        }
                                    },
                                    "required": [
                                        "data"
                                    ]
                                }
                            }
                        }
                    },
                    "404": {
                        "$ref": "#/components/responses/ModelNotFoundException"
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/user/profile": {
            "get": {
                "operationId": "user.profile",
                "description": "Eager-loads `role`, `brand`, and `influencer` regardless of\nwhich role is actually active \u2014 so a user who has switched\nroles via `updateProfile()` and accumulated both a `Brand` and\nan `Influencer` row (see above) would see **both** populated in\nthis response simultaneously, with only the top-level `role`\nfield indicating which one is \"current.\" Any frontend consuming\nthis response needs to branch on `role`, not on which of\n`brand`/`influencer` happens to be non-null, since after a role\nswitch both can be.\n\nLike `updateProfile()`, returns the request's own bearer token\nrather than issuing or refreshing one.",
                "summary": "Get authenticated user profile",
                "tags": [
                    "User Management"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/user/update-profile": {
            "put": {
                "operationId": "user.updateProfile",
                "description": "Doubles as both a profile-update endpoint and a **role-assignment**\nendpoint: the effective `role` for this request is taken from\nthe request body if present, falling back to the user's\n*current* role only if the body omits it. There is no check\nanywhere in this method preventing a user from switching their\nown role on every call \u2014 a user currently registered as an\n`influencer` can submit `role: brand` along with brand-shaped\nfields and this method will create a `Brand` profile for them\nand update `role_id` accordingly, without any confirmation step,\nwithout preventing the reverse, and without checking whether\nthey already have data under the *other* role. Switching back\nand forth would leave both a `Brand` and an `Influencer` row\npermanently attached to the same user (via `updateOrCreate`,\nwhich never deletes the other role's row), with only `role_id`\nindicating which one is currently \"active.\"\n\n**Validation is two-tiered and merged.** `$baseRules` apply\nregardless of role; `$roleRules` is one of two completely\ndifferent rule sets selected by `$role` \u2014 brand rules include\ncompany/registration/social-handle/audience-targeting fields,\ninfluencer rules include platform/demographics/content-taxonomy/\nengagement-metric fields. Submitting brand-shaped fields while\n`role = influencer` (or vice versa) does not fail validation \u2014\nthose keys are simply absent from the active rule set and are\nsilently ignored later when `$request->only([...])` extracts\nonly the fields relevant to the resolved role.\n\n**JSON re-decoding workaround.** `primary_products`,\n`target_audience_location`, and `target_audience_interests` are\nvalidated as plain strings/arrays but explicitly re-decoded\nfrom JSON-encoded strings before being saved, because the\nfrontend pre-encodes these fields as JSON strings before\nsending them \u2014 this decode step exists specifically to prevent\nthe model's `array` cast from double-encoding an already-JSON\nstring into a stored string-of-a-string. If the frontend's\nencoding behavior ever changes for these three fields without\na corresponding change here, this is the first place to check.\nNotably, this re-decode is applied only to brand fields; the\ninfluencer branch's array fields\n(`social_media_links`, `content_types`, etc.) are taken via\n`$request->input(..., [])` directly with no equivalent decode\nstep, so if the frontend encodes those the same way, they would\nnot get the same protection.\n\n**Influencer screenshot handling preserves existing files on\npartial updates.** For each of the three screenshot fields, a\nnewly uploaded file is stored and its path used; if no file is\nuploaded in this request, the *existing* stored path is reused\nfrom `$user->influencer?->{$field}` rather than being cleared \u2014\nso an influencer can update unrelated fields without\naccidentally wiping out previously uploaded screenshots, but\nthere is also no way to explicitly *remove* a screenshot via\nthis endpoint; the only way to change one is to upload a\nreplacement. The old file at the previous path is never deleted\nfrom storage when replaced (contrast with\n`CampaignController::update()`, which does delete the\nold cover image before storing a new one) \u2014 replaced\nscreenshots accumulate as orphaned files on the `public` disk\nindefinitely.\n\n**AI sync side effect.** Saving either a `Brand` or\n`Influencer` row via `updateOrCreate()` fires that model's\nobserver automatically on every save (per the inline comment),\nwhich pushes the updated profile to the AI matching engine \u2014\nmeaning every profile update, however minor, triggers an AI\nresync; there's no batching or debounce visible here for a user\nwho updates their profile several times in quick succession.\n\n**Response includes the request's own bearer token, not a new\none** \u2014 `$request->bearerToken()` echoes back whatever token\nthe caller already authenticated with; this endpoint does not\nissue a new token even when it just changed the user's role,\nso any token-embedded role claims (if the auth system has any)\nwould be stale until the user's next independent token refresh.",
                "summary": "Update Brand or Influencer profile",
                "tags": [
                    "User Management"
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "role": {
                                        "type": "string"
                                    },
                                    "social_media_links": {
                                        "type": "string"
                                    },
                                    "primary_platform": {
                                        "type": "string"
                                    },
                                    "follower_count": {
                                        "type": "string",
                                        "default": 0
                                    },
                                    "engagement_rate": {
                                        "type": "string",
                                        "default": 0
                                    },
                                    "avg_views": {
                                        "type": "string",
                                        "default": 0
                                    },
                                    "secondary_platforms": {
                                        "type": "string"
                                    },
                                    "profile_bio": {
                                        "type": "string"
                                    },
                                    "portfolio_links": {
                                        "type": "string"
                                    },
                                    "content_types": {
                                        "type": "string"
                                    },
                                    "content_styles": {
                                        "type": "string"
                                    },
                                    "content_niche": {
                                        "type": "string"
                                    },
                                    "age_demographics": {
                                        "type": "string"
                                    },
                                    "gender_demographics": {
                                        "type": "string"
                                    },
                                    "location_demographics": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "anyOf": [
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "Validation failed."
                                                },
                                                "errors": {
                                                    "type": "string"
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "success": {
                                                    "type": "boolean"
                                                },
                                                "message": {
                                                    "type": "string",
                                                    "const": "Invalid or missing role. Must be brand or influencer."
                                                },
                                                "errors": {
                                                    "type": "array",
                                                    "items": {
                                                        "type": "string"
                                                    },
                                                    "minItems": 0,
                                                    "maxItems": 0,
                                                    "additionalItems": false
                                                }
                                            },
                                            "required": [
                                                "success",
                                                "message",
                                                "errors"
                                            ]
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/user/sync-ai": {
            "post": {
                "operationId": "user.syncAi",
                "description": "The same AI-sync-via-`touch()` pattern as\n`CampaignController::syncAi()`, applied to the user's own\n`Brand` or `Influencer` row instead of a campaign. Resolves\nwhich model to touch from the user's *current* `role_id`\nrather than checking both relations \u2014 so if a user has\nswitched roles via `updateProfile()` and ended up with stale\ndata under their *previous* role's model (per the dual-row\nscenario described above), this endpoint has no way to target\nthat orphaned profile; it only ever syncs whichever relation\nmatches the currently active role.\n\nReturns a 422 if the resolved role has no corresponding\nprofile row yet (e.g. a brand-role user who hasn't completed\n`updateProfile()` for the first time), distinguishing\n\"nothing to sync\" from a hard server error.",
                "summary": "Trigger AI sync for the authenticated user's brand or influencer profile",
                "tags": [
                    "User Management"
                ],
                "responses": {
                    "422": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "No profile found to sync. Complete your profile first."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/AuthenticationException"
                    }
                }
            }
        },
        "/billboard/users": {
            "get": {
                "operationId": "userApi.index",
                "description": "Every user on the platform, with `role`, `brand`, and\n`influencer` eagerly loaded \u2014 no pagination, no filtering by\nrole/status, and (per the class-level note) no Resource\nwrapping. For a platform of any meaningful size this returns an\nunbounded, full-table response on every call; contrast with\n`TransactionsController::index()`, which paginates by default\neven for a single user's own data.",
                "summary": "Get all users",
                "tags": [
                    "User Management"
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/users/{id}": {
            "get": {
                "operationId": "userApi.show",
                "description": "One user by ID with the same three relations loaded, same raw\nmodel exposure as every other method here.",
                "summary": "Get specific user",
                "tags": [
                    "User Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "User not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            },
            "put": {
                "operationId": "userApi.update_1",
                "description": "Updates base profile fields and, separately, the user's role \u2014\n**including promotion to `Admin`**, which is one of the three\nvalues accepted by the `role` validation rule alongside `Brand`\nand `Influencer`. This is the only place in the entire API\n(v1 or admin) where `Admin` appears as an assignable role value;\n`v1\\UserController::updateProfile()`'s own role-switching only\never allows `brand`/`influencer`. There is no check here\npreventing an admin from granting `Admin` to an arbitrary user,\nnor any check preventing the *last remaining* admin from\ndemoting themselves out of the role entirely (if `role` here\nhappens to be the same account making the request) \u2014 both\nscenarios are accepted identically to any other role change.\n\n`email` uniqueness is validated correctly, excluding the\ncurrent user's own row (`unique:users,email,{$id}`) so a user\ncan keep their existing email unchanged without tripping the\nuniqueness rule against themselves.\n\nThe role update is a **separate** `$user->update(['role_id' =>\n...])` call after the main profile update, rather than being\nmerged into one write \u2014 two database writes for what's\npresented as a single \"update user\" action, with no transaction\nwrapping the two; a failure between them would leave profile\nfields changed but the role unchanged, or vice versa depending\non which one ran first.\n\nChecks `if ($request->role)` rather than\n`$request->filled('role')` or `$request->has('role')` \u2014 a\nrequest explicitly sending `role: \"\"` (empty string) or\n`role: 0` would both evaluate falsy and skip the role update\nsilently, which is probably the intended behavior for \"no role\n     * change requested\" but is a slightly different check than the\nvalidation rule above it (`sometimes|string|in:...`) would\nsuggest at a glance.",
                "summary": "Update user",
                "tags": [
                    "User Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "full_name": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "user_name": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "email": {
                                        "type": "string",
                                        "format": "email"
                                    },
                                    "phone": {
                                        "type": "string",
                                        "maxLength": 20
                                    },
                                    "country": {
                                        "type": "string",
                                        "maxLength": 100
                                    },
                                    "city": {
                                        "type": "string",
                                        "maxLength": 100
                                    },
                                    "role": {
                                        "type": "string",
                                        "enum": [
                                            "Brand",
                                            "Influencer",
                                            "Admin"
                                        ]
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "User not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            },
            "patch": {
                "operationId": "userApi.update_2",
                "description": "Updates base profile fields and, separately, the user's role \u2014\n**including promotion to `Admin`**, which is one of the three\nvalues accepted by the `role` validation rule alongside `Brand`\nand `Influencer`. This is the only place in the entire API\n(v1 or admin) where `Admin` appears as an assignable role value;\n`v1\\UserController::updateProfile()`'s own role-switching only\never allows `brand`/`influencer`. There is no check here\npreventing an admin from granting `Admin` to an arbitrary user,\nnor any check preventing the *last remaining* admin from\ndemoting themselves out of the role entirely (if `role` here\nhappens to be the same account making the request) \u2014 both\nscenarios are accepted identically to any other role change.\n\n`email` uniqueness is validated correctly, excluding the\ncurrent user's own row (`unique:users,email,{$id}`) so a user\ncan keep their existing email unchanged without tripping the\nuniqueness rule against themselves.\n\nThe role update is a **separate** `$user->update(['role_id' =>\n...])` call after the main profile update, rather than being\nmerged into one write \u2014 two database writes for what's\npresented as a single \"update user\" action, with no transaction\nwrapping the two; a failure between them would leave profile\nfields changed but the role unchanged, or vice versa depending\non which one ran first.\n\nChecks `if ($request->role)` rather than\n`$request->filled('role')` or `$request->has('role')` \u2014 a\nrequest explicitly sending `role: \"\"` (empty string) or\n`role: 0` would both evaluate falsy and skip the role update\nsilently, which is probably the intended behavior for \"no role\n     * change requested\" but is a slightly different check than the\nvalidation rule above it (`sometimes|string|in:...`) would\nsuggest at a glance.",
                "summary": "Update user",
                "tags": [
                    "User Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "full_name": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "user_name": {
                                        "type": "string",
                                        "maxLength": 255
                                    },
                                    "email": {
                                        "type": "string",
                                        "format": "email"
                                    },
                                    "phone": {
                                        "type": "string",
                                        "maxLength": 20
                                    },
                                    "country": {
                                        "type": "string",
                                        "maxLength": 100
                                    },
                                    "city": {
                                        "type": "string",
                                        "maxLength": 100
                                    },
                                    "role": {
                                        "type": "string",
                                        "enum": [
                                            "Brand",
                                            "Influencer",
                                            "Admin"
                                        ]
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "User not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    },
                    "422": {
                        "$ref": "#/components/responses/ValidationException"
                    }
                }
            },
            "delete": {
                "operationId": "userApi.destroy",
                "description": "A hard delete of the `User` row with no cleanup of related data\nvisible in this method \u2014 no removal or reassignment of the\nuser's `Brand`/`Influencer` profile, no handling of their\n`Ledger` history, `Campaign`s (if they're a brand), \n`CampaignApplication`s (if they're an influencer), or\n`PersonalAccessToken`s. Whether any of that cascades depends\nentirely on database-level foreign key constraints or model\nevents outside this controller \u2014 the same category of \"does the\n     * docblock's claim hold without an external cascade\" gap flagged\nin `Api\\LocationApiController::destroy()`. Deleting a brand\nuser with active campaigns, for instance, would either cascade\nthose campaigns away (silently removing them from influencers'\nview) or leave them pointing at a brand that no longer exists,\ndepending on what isn't visible from this method.",
                "summary": "Delete user",
                "tags": [
                    "User Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "User not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/users/{id}/suspend": {
            "post": {
                "operationId": "userApi.suspend",
                "description": "Sets `is_suspended = true` directly with no check on the\nuser's current state \u2014 calling this on an already-suspended\nuser is a harmless no-op in effect, but also no error, so a\ncaller has no way to distinguish \"I just suspended them\" from\n\"they were already suspended\" from this response alone (the\nsuccess message is identical either way).\n\nCritically, this method **does not revoke the user's existing\nSanctum tokens** \u2014 suspending a user updates a flag on the\n`User` row, but any bearer token issued before suspension\n(from `AuthController::login()`, `SocialAuthController`, or\n`TiktokAuthController`) remains valid unless something *else* in\nthe request pipeline (e.g. middleware checking `is_suspended`\non every authenticated request) actively rejects it. If no such\nmiddleware exists, a suspended user retains full API access\nwith their existing token until it would otherwise expire \u2014 the\nsuspension only takes effect the next time they'd need to\n*re-authenticate*, not immediately. Worth verifying this\ndirectly, since \"suspend\" implying \"immediately cut off\" is the\nnatural reading of the word and the actual behavior may not\nmatch it.",
                "summary": "Suspend user",
                "tags": [
                    "User Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "User not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billboard/users/{id}/activate": {
            "post": {
                "operationId": "userApi.activate",
                "description": "The inverse of `suspend()` \u2014 sets `is_suspended = false`, same\nno-op-on-already-active behavior, same lack of a state check.",
                "summary": "Activate user",
                "tags": [
                    "User Management"
                ],
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "success": {
                                            "type": "boolean"
                                        },
                                        "message": {
                                            "type": "string",
                                            "const": "User not found."
                                        },
                                        "errors": {
                                            "type": "array",
                                            "items": {
                                                "type": "string"
                                            },
                                            "minItems": 0,
                                            "maxItems": 0,
                                            "additionalItems": false
                                        }
                                    },
                                    "required": [
                                        "success",
                                        "message",
                                        "errors"
                                    ]
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "http": {
                "type": "http",
                "scheme": "bearer",
                "bearerFormat": "token"
            }
        },
        "schemas": {
            "ApplicationDeliverableResource": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string"
                    },
                    "campaign_id": {
                        "type": "string"
                    },
                    "deliverable_ref_id": {
                        "type": "string"
                    },
                    "platform": {
                        "type": "string"
                    },
                    "content_type": {
                        "type": "string"
                    },
                    "description": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "sort_order": {
                        "type": "integer"
                    },
                    "status": {
                        "type": "string"
                    },
                    "created_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "updated_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    }
                },
                "required": [
                    "id",
                    "campaign_id",
                    "deliverable_ref_id",
                    "platform",
                    "content_type",
                    "description",
                    "sort_order",
                    "status",
                    "created_at",
                    "updated_at"
                ],
                "title": "ApplicationDeliverableResource"
            },
            "CampaignApplicationResource": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer"
                    },
                    "campaign_id": {
                        "type": "string"
                    },
                    "campaign_title": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "campaign_brand": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "influencer_id": {
                        "type": "integer"
                    },
                    "influencer_name": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "influencer_email": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "status": {
                        "type": "string"
                    },
                    "rejection_reason": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "reviewed_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "approved_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "total_deliverables_required": {
                        "type": "integer"
                    },
                    "total_deliverables_submitted": {
                        "type": "integer"
                    },
                    "total_deliverables_approved": {
                        "type": "integer"
                    },
                    "completion_percentage": {
                        "type": "integer"
                    },
                    "current_milestone": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "applied_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "submission_deadline": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "deliverables_count": {
                        "anyOf": [
                            {
                                "type": "integer"
                            },
                            {
                                "type": "integer",
                                "enum": [
                                    0
                                ]
                            }
                        ]
                    },
                    "created_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "updated_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    }
                },
                "required": [
                    "id",
                    "campaign_id",
                    "campaign_title",
                    "campaign_brand",
                    "influencer_id",
                    "influencer_name",
                    "influencer_email",
                    "status",
                    "rejection_reason",
                    "reviewed_at",
                    "approved_at",
                    "total_deliverables_required",
                    "total_deliverables_submitted",
                    "total_deliverables_approved",
                    "completion_percentage",
                    "current_milestone",
                    "applied_at",
                    "submission_deadline",
                    "deliverables_count",
                    "created_at",
                    "updated_at"
                ],
                "title": "CampaignApplicationResource"
            },
            "CampaignResource": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string"
                    },
                    "brand_id": {
                        "type": "string"
                    },
                    "brand_name": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "title": {
                        "type": "string"
                    },
                    "description": {
                        "type": "string"
                    },
                    "products": {
                        "anyOf": [
                            {
                                "type": "array",
                                "items": {}
                            },
                            {
                                "type": "null"
                            },
                            {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "minItems": 0,
                                "maxItems": 0,
                                "additionalItems": false
                            }
                        ]
                    },
                    "dos_for_creators": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "donts_for_creators": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "preferred_platforms": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "requirements": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "industry": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "budget": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "payment_plan": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "payment_price_per_creator": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "payment_price_per_view": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "payment_model": {
                        "type": "string"
                    },
                    "engagement_rate_per_point": {
                        "type": "string"
                    },
                    "total_engagement_score": {
                        "type": "string"
                    },
                    "total_engagement_payment": {
                        "type": "string"
                    },
                    "cover_image_url": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "target_audience_description": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "target_audience_age_range": {
                        "type": "string"
                    },
                    "target_audience_gender": {
                        "type": "string"
                    },
                    "required_content_types": {
                        "anyOf": [
                            {
                                "type": "string"
                            },
                            {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "minItems": 0,
                                "maxItems": 0,
                                "additionalItems": false
                            }
                        ]
                    },
                    "required_content_styles": {
                        "anyOf": [
                            {
                                "type": "string"
                            },
                            {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "minItems": 0,
                                "maxItems": 0,
                                "additionalItems": false
                            }
                        ]
                    },
                    "timeline_start_date": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "timeline_end_date": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "campaign_goals": {
                        "anyOf": [
                            {
                                "type": "array",
                                "items": {}
                            },
                            {
                                "type": "null"
                            },
                            {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "minItems": 0,
                                "maxItems": 0,
                                "additionalItems": false
                            }
                        ]
                    },
                    "primary_cta": {
                        "type": "string"
                    },
                    "website_url": {
                        "type": "string"
                    },
                    "required_hashtags": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "required_mentions": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "deliverables": {
                        "anyOf": [
                            {
                                "type": "array",
                                "items": {}
                            },
                            {
                                "type": "null"
                            },
                            {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "minItems": 0,
                                "maxItems": 0,
                                "additionalItems": false
                            }
                        ]
                    },
                    "application_deliverables": {
                        "anyOf": [
                            {
                                "type": "array",
                                "items": {
                                    "$ref": "#/components/schemas/ApplicationDeliverableResource"
                                }
                            },
                            {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "minItems": 0,
                                "maxItems": 0,
                                "additionalItems": false
                            }
                        ]
                    },
                    "status": {
                        "type": "string"
                    },
                    "brief_status": {
                        "anyOf": [
                            {
                                "type": "string"
                            },
                            {
                                "type": "string",
                                "enum": [
                                    "none"
                                ]
                            }
                        ]
                    },
                    "ai_brief_draft": {
                        "anyOf": [
                            {
                                "type": "string"
                            },
                            {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "minItems": 0,
                                "maxItems": 0,
                                "additionalItems": false
                            }
                        ]
                    },
                    "campaign_type": {
                        "type": "string"
                    },
                    "service_type": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "created_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "updated_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    }
                },
                "required": [
                    "id",
                    "brand_id",
                    "brand_name",
                    "title",
                    "description",
                    "products",
                    "dos_for_creators",
                    "donts_for_creators",
                    "preferred_platforms",
                    "requirements",
                    "industry",
                    "budget",
                    "payment_plan",
                    "payment_price_per_creator",
                    "payment_price_per_view",
                    "payment_model",
                    "engagement_rate_per_point",
                    "total_engagement_score",
                    "total_engagement_payment",
                    "cover_image_url",
                    "target_audience_description",
                    "target_audience_age_range",
                    "target_audience_gender",
                    "required_content_types",
                    "required_content_styles",
                    "timeline_start_date",
                    "timeline_end_date",
                    "campaign_goals",
                    "primary_cta",
                    "website_url",
                    "required_hashtags",
                    "required_mentions",
                    "deliverables",
                    "application_deliverables",
                    "status",
                    "brief_status",
                    "ai_brief_draft",
                    "campaign_type",
                    "service_type",
                    "created_at",
                    "updated_at"
                ],
                "title": "CampaignResource"
            },
            "NotificationResource": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string"
                    },
                    "type": {
                        "type": "string"
                    },
                    "data": {
                        "type": "array",
                        "items": {}
                    },
                    "read": {
                        "type": "boolean"
                    },
                    "created_at": {
                        "type": "string"
                    }
                },
                "required": [
                    "id",
                    "type",
                    "data",
                    "read",
                    "created_at"
                ],
                "title": "NotificationResource"
            },
            "RecruitmentResource": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string"
                    },
                    "full_name": {
                        "type": "string"
                    },
                    "email": {
                        "type": "string"
                    },
                    "phone": {
                        "type": "string"
                    },
                    "location": {
                        "type": "string"
                    },
                    "role": {
                        "type": "string"
                    },
                    "why_join": {
                        "type": "string"
                    },
                    "resume_url": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "portfolio_files": {
                        "anyOf": [
                            {
                                "type": "array",
                                "items": {}
                            },
                            {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "minItems": 0,
                                "maxItems": 0,
                                "additionalItems": false
                            }
                        ]
                    },
                    "portfolio_url": {
                        "type": "string"
                    },
                    "years_product_management": {
                        "type": "string"
                    },
                    "product_description": {
                        "type": "string"
                    },
                    "linkedin": {
                        "type": "string"
                    },
                    "github": {
                        "type": "string"
                    },
                    "years_laravel": {
                        "type": "string"
                    },
                    "years_react": {
                        "type": "string"
                    },
                    "payment_integration": {
                        "type": "string"
                    },
                    "years_advisory": {
                        "type": "string"
                    },
                    "startups_scaled": {
                        "type": "string"
                    },
                    "investor_network_size": {
                        "type": "string"
                    },
                    "status": {
                        "type": "string"
                    },
                    "created_at": {
                        "type": "string"
                    },
                    "updated_at": {
                        "type": "string"
                    }
                },
                "required": [
                    "id",
                    "full_name",
                    "email",
                    "phone",
                    "location",
                    "role",
                    "why_join",
                    "resume_url",
                    "portfolio_files",
                    "portfolio_url",
                    "years_product_management",
                    "product_description",
                    "linkedin",
                    "github",
                    "years_laravel",
                    "years_react",
                    "payment_integration",
                    "years_advisory",
                    "startups_scaled",
                    "investor_network_size",
                    "status",
                    "created_at",
                    "updated_at"
                ],
                "title": "RecruitmentResource"
            },
            "TransactionResource": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string"
                    },
                    "type": {
                        "type": "string"
                    },
                    "amount": {
                        "type": "string"
                    },
                    "currency": {
                        "type": "string"
                    },
                    "status": {
                        "type": "string"
                    },
                    "reference": {
                        "type": "string"
                    },
                    "description": {
                        "type": "string"
                    },
                    "created_at": {
                        "type": "string"
                    },
                    "updated_at": {
                        "type": "string"
                    }
                },
                "required": [
                    "id",
                    "type",
                    "amount",
                    "currency",
                    "status",
                    "reference",
                    "description",
                    "created_at",
                    "updated_at"
                ],
                "title": "TransactionResource"
            }
        },
        "responses": {
            "AuthenticationException": {
                "description": "Unauthenticated",
                "content": {
                    "application/json": {
                        "schema": {
                            "type": "object",
                            "properties": {
                                "message": {
                                    "type": "string",
                                    "description": "Error overview."
                                }
                            },
                            "required": [
                                "message"
                            ]
                        }
                    }
                }
            },
            "ValidationException": {
                "description": "Validation error",
                "content": {
                    "application/json": {
                        "schema": {
                            "type": "object",
                            "properties": {
                                "message": {
                                    "type": "string",
                                    "description": "Errors overview."
                                },
                                "errors": {
                                    "type": "object",
                                    "description": "A detailed description of each field that failed validation.",
                                    "additionalProperties": {
                                        "type": "array",
                                        "items": {
                                            "type": "string"
                                        }
                                    }
                                }
                            },
                            "required": [
                                "message",
                                "errors"
                            ]
                        }
                    }
                }
            },
            "AuthorizationException": {
                "description": "Authorization error",
                "content": {
                    "application/json": {
                        "schema": {
                            "type": "object",
                            "properties": {
                                "message": {
                                    "type": "string",
                                    "description": "Error overview."
                                }
                            },
                            "required": [
                                "message"
                            ]
                        }
                    }
                }
            },
            "ModelNotFoundException": {
                "description": "Not found",
                "content": {
                    "application/json": {
                        "schema": {
                            "type": "object",
                            "properties": {
                                "message": {
                                    "type": "string",
                                    "description": "Error overview."
                                }
                            },
                            "required": [
                                "message"
                            ]
                        }
                    }
                }
            }
        }
    }
}