{
  "$meta": {
    "purpose": "English overlay applied on top of openapi.json when the docs language is switched to EN. Only fields present here override the raw spec; anything missing falls through to the Japanese original.",
    "updated": "2026-04-19",
    "coverageNote": "All current operations are covered (summary-level). Add `description` entries as needed for deeper docs."
  },
  "info": {
    "title": "Parky API",
    "description": "## English\nParky BFF — the `/v1/*` API running on Cloudflare Workers. Every client (mobile, admin portal, public web) hits only this API.\n\nAll route summaries are available in English via the overlay at `docs/i18n/openapi-en-overlay.json`. Route-level descriptions beyond summaries may still appear in Japanese in a few places."
  },
  "tags": [
    { "name": "codes",               "description": "Code master (enum labels shared across clients)." },
    { "name": "me",                  "description": "Authenticated user's profile, settings, and personal data." },
    { "name": "parking-lots",        "description": "Public parking-lot catalog — list, search, detail." },
    { "name": "parking-sessions",    "description": "Live parking-session lifecycle (start / end / fee calc)." },
    { "name": "reviews",             "description": "Public reviews for parking lots." },
    { "name": "notifications",       "description": "End-user notification inbox + FCM push tokens." },
    { "name": "articles",            "description": "Editorial articles and media." },
    { "name": "ads",                 "description": "Public ad slots." },
    { "name": "support",             "description": "User-facing support tickets." },
    { "name": "error-reports",       "description": "User-submitted error reports for parking data." },
    { "name": "sponsors",            "description": "Sponsor directory and check-in tracking." },
    { "name": "gamification",        "description": "EXP, levels, and badges." },
    { "name": "themes",              "description": "UI themes / gift themes." },
    { "name": "storage",             "description": "Cloudflare R2 presigned PUT URLs and asset metadata." },
    { "name": "subscriptions",       "description": "Premium plan subscriptions." },
    { "name": "search",              "description": "AI-powered natural-language search." },
    { "name": "search-lots",         "description": "Public parking-lot dump endpoint used by static builds." },
    { "name": "hubs",                "description": "Public station hubs (station + nearby parking lots)." },
    { "name": "tags",                "description": "Tag taxonomy for parking-lot attributes." },
    { "name": "user-vehicles",       "description": "End-user vehicle registrations." },
    { "name": "user-notifications",  "description": "Admin UI for composing / sending user notifications (via FCM dispatch queue)." },
    { "name": "admin",               "description": "Admin-only endpoints (RBAC-gated)." },
    { "name": "admins",              "description": "Admin user CRUD with Supabase Auth provisioning." },
    { "name": "roles",               "description": "RBAC roles and role-permission mappings." },
    { "name": "assets",              "description": "Admin asset metadata management (R2-backed)." },
    { "name": "operators",           "description": "Parking-lot operators." },
    { "name": "owners",              "description": "Owner accounts + parking-lot ownership claims." },
    { "name": "audit",               "description": "Admin activity audit logs." },
    { "name": "revenue",             "description": "Revenue aggregation, transactions, and subscription counts." },
    { "name": "stores",              "description": "Play Store / App Store sync — integrations, sales, metrics, reviews, triggers." },
    { "name": "badges",              "description": "Badge catalog with tag replacement and backfill." },
    { "name": "ai",                  "description": "AI provider configuration and usage logs." },
    { "name": "dashboard",           "description": "Admin dashboard aggregates." },
    { "name": "boosts",              "description": "Sponsor boost campaigns." },
    { "name": "tasks",               "description": "Admin task list / upsert by reference." },
    { "name": "users",               "description": "Admin-side user listing and bulk operations." },
    { "name": "instagram",           "description": "Instagram automation tool (isolated D1 + R2 bucket)." }
  ],
  "paths": {
    "/healthz": {
      "get": { "summary": "Health check", "description": "Returns `{ ok: true, time }` — used by CI and uptime monitors." }
    },

    "/v1/codes": {
      "get": { "summary": "Bulk-fetch code masters (across categories)" }
    },
    "/v1/me": {
      "get": {
        "summary": "Get my profile and permission scopes",
        "description": "Returns the authenticated user's profile. Call this once at app startup to bootstrap client state.\n\n- **Auth**: required. `user_id` is derived from the JWT `sub` and looked up in `app_users`.\n- **scopes**: feature gates computed server-side each call (e.g. `premium.unlimited_saves`).\n- **Failure modes**: 401 if the JWT is invalid; if there's no `app_users` row yet (fresh sign-up) the row is auto-provisioned and returned."
      },
      "patch": {
        "summary": "Update my profile",
        "description": "Patches user-editable profile fields (display name, avatar, etc.).\n\n- **Partial**: only send fields you want changed. Explicit `null` clears the value; omission keeps the current one.\n- **Validation**: length caps and banned-word checks via Zod. Failures return 422 with `issues[]`.\n- **avatar**: send `asset_id`. Upload the file to R2 via `/v1/storage/upload-url` first, then attach it here."
      }
    },
    "/v1/me/withdraw": {
      "post": {
        "summary": "Withdraw account (anonymize + status change)",
        "description": "Withdraws the current user's account, anonymizing PII while preserving referential integrity.\n\n- **Effect**: nulls `display_name` / `email` / `avatar_asset_id` (or replaces them with anonymized values) and sets `status='withdrawn'`.\n- **Side effect**: also deletes the Supabase Auth user via `admin_auth.deleteUser` (run with Service Role on Workers).\n- **Irreversible**: always confirm on the client before calling.\n- **Response**: 204 No Content. Any subsequent request will return 401."
      }
    },
    "/v1/me/saved-parking-lots": {
      "get":  { "summary": "List my saved parking lots" },
      "post": { "summary": "Save a parking lot" }
    },
    "/v1/me/saved-parking-lots/{lotId}": {
      "delete": { "summary": "Unsave a parking lot" }
    },
    "/v1/me/vehicles": {
      "get":  { "summary": "List my vehicles" },
      "post": { "summary": "Register a vehicle" }
    },
    "/v1/me/vehicles/{id}": {
      "patch":  { "summary": "Update a vehicle" },
      "delete": { "summary": "Soft-delete a vehicle" }
    },
    "/v1/me/parking-sessions": {
      "get": { "summary": "List my parking sessions" }
    },
    "/v1/me/parking-sessions/{id}": {
      "get":   { "summary": "Get my parking session detail" },
      "patch": { "summary": "Update memo / personal rating" }
    },
    "/v1/me/reviews/{id}": {
      "patch": { "summary": "Edit my review (resets to status=pending for re-moderation)" }
    },
    "/v1/reviews": {
      "get": { "summary": "List published reviews across lots (status=approved, for SSG build)" }
    },
    "/v1/me/notifications": {
      "get": {
        "summary": "List my notifications",
        "description": "Returns the authenticated user's `user_notifications` in reverse chronological order.\n\n- **Auth**: required. The `user_id` comes from the JWT — we never return someone else's notifications (enforced at the code layer *and* RLS).\n- **Paging**: `page` / `limit` (max 100). Pull-to-refresh fetches the top; scroll-end fetches the next page.\n- **Realtime**: pair this with a supabase-flutter Realtime subscription on the same filter to receive INSERTs live (data-plane exception)."
      }
    },
    "/v1/me/notifications/mark-read": {
      "post": {
        "summary": "Mark notifications as read (by IDs or `before`; both null marks all)",
        "description": "Marks notifications as read by writing the current timestamp into `read_at`.\n\n- **Modes**: `ids` targets specific rows; `before` marks everything up to that timestamp; both null marks all of the caller's unread.\n- **Idempotent**: calling on already-read rows is a no-op (doesn't overwrite `read_at`).\n- **Response**: `updated` is the affected row count — the client can use it to diff-update the unread badge."
      }
    },
    "/v1/me/push-tokens": {
      "put": {
        "summary": "Upsert my FCM device token",
        "description": "Registers the device's FCM token on the server — a prerequisite for push delivery.\n\n- **When to call**: immediately after app launch and on every `onTokenRefresh`. FCM tokens can rotate.\n- **Upsert**: unique on `(user_id, device_type)`; re-registration overwrites the token.\n- **Sign-out**: delete the token from the client to prevent mis-delivery.\n- **Status**: server-side dispatch (the `parky-fcm-dispatch` queue + consumer) is complete; wiring `firebase_messaging` on the Flutter side is the remaining task."
      }
    },
    "/v1/me/exp": {
      "get": { "summary": "Get my EXP / level (includes EXP remaining to next level)" }
    },
    "/v1/me/badges": {
      "get": { "summary": "List earned badges" }
    },
    "/v1/me/badge-progress": {
      "get": { "summary": "List in-progress badges (unearned only)" }
    },
    "/v1/me/themes": {
      "get": { "summary": "List themes I own" }
    },
    "/v1/me/themes/{id}/apply": {
      "post": { "summary": "Apply an owned theme (updates `app_users.active_theme_id`)" }
    },
    "/v1/me/subscription": {
      "get": { "summary": "Get my current subscription (null fields if no active sub)" }
    },
    "/v1/me/subscription/verify-iap": {
      "post": { "summary": "Verify IAP receipt and update `user_subscriptions`" }
    },
    "/v1/parking-lots": {
      "get": {
        "summary": "Search parking lots (filter + paging)",
        "description": "Lists published parking lots, filtered by name / address / status.\n\n- **Auth**: optional (`optionalUser`). Results are identical across users — JWT only adds context.\n- **Paging**: `page` / `limit` (default 20/page). Response is `{ items, page, limit, total }`.\n- **Filters**: `q` does an ILIKE match on `name` / `address`; `status` is an exact code-value match (e.g. `active`).\n- **Cache**: 5 min at the Cloudflare edge (`s-maxage=300`), 30 s in the browser.\n- **Not for geo**: for radius search use `/v1/parking-lots/nearby` instead."
      }
    },
    "/v1/parking-lots/nearby": {
      "get": {
        "summary": "Parking lots near a GPS coordinate (PostGIS RPC)",
        "description": "Returns parking lots within a radius of the given `lat` / `lng`, sorted by distance ascending.\n\n- **Engine**: the `nearby_parking_lots` PostgreSQL RPC (PostGIS `ST_DWithin` + `ST_Distance`).\n- **Radius**: `radius_m` defaults to 1000 m. Must be a positive number; invalid values return 400.\n- **Response**: each row includes `distance_m` so the client can show distances and order.\n- **Cache**: 60 s at the edge, 30 s in the browser. TTL is short because GPS-driven calls change quickly.\n- **Mobile use cases**: initial map load, \"nearby\" button, parking-start candidate list."
      }
    },
    "/v1/parking-lots/{id}": {
      "get": {
        "summary": "Parking-lot detail (includes images, tags, attributes)",
        "description": "Returns the detail view of one parking lot — used by the mobile detail screen and the Web SSG individual page.\n\n- **include**: comma-separated list of `images` / `tags` / `pricing_rules` / `operator`. Omit it for a leaner payload.\n- **Dimensions & constraints**: includes `max_height_m` / `max_width_m` / `max_length_m` / `max_weight_t` / `min_clearance_cm` / `max_parking_duration_min` so clients can run vehicle-fit checks themselves.\n- **operator**: included only when the `operator` include flag is on; returns `{ id, name, slug, color }`.\n- **Cache**: 5 min at the edge. Unknown IDs return 404."
      }
    },
    "/v1/parking-lots/{id}/pricing-rules": {
      "get": { "summary": "Pricing rules for a parking lot" }
    },
    "/v1/parking-lots/{id}/nearby-stations": {
      "get": { "summary": "Top 5 nearest stations to a parking lot (by distance ASC)" }
    },
    "/v1/parking-lots/{id}/images": {
      "get": { "summary": "List images for a parking lot" }
    },
    "/v1/parking-lots/{id}/calc-fee": {
      "post": {
        "summary": "Estimate fee between entry and exit (RPC `calc_parking_fee`)",
        "description": "Estimates the fee for parking in a given lot between `entry_at` and `exit_at`.\n\n- **Engine**: the `calc_parking_fee` PostgreSQL RPC evaluates time-of-day rules, caps, and vehicle-type surcharges in one go.\n- **Response**: `total_amount` (integer JPY) and `breakdown` (per-window line items). The mobile fee-simulator screen renders this.\n- **Idempotent**: no side effects — same inputs always return the same result.\n- **Auth**: not required. Guests can estimate; starting an actual session requires auth via a different endpoint."
      }
    },
    "/v1/parking-lots/{id}/reviews": {
      "get":  { "summary": "List approved reviews for a parking lot" },
      "post": {
        "summary": "Post a review (created with status=pending)",
        "description": "Posts a star rating and optional comment to the given parking lot as the authenticated user.\n\n- **Auth**: required (Supabase JWT) — must pass `requireUser`.\n- **Initial status**: `pending`. Admin moderation flips it to `approved` / `rejected`; only `approved` rows appear in the public list.\n- **Photos**: not accepted on this endpoint. Upload photos separately via `/v1/storage/upload-url` and attach the resulting `asset_id`.\n- **Duplicates**: if the user already has a review for this lot, have the client fall back to editing (`PATCH /v1/me/reviews/{id}`)."
      }
    },
    "/v1/parking-sessions": {
      "post": {
        "summary": "Start parking session (RPC `create_parking_session`)",
        "description": "Records that the authenticated user has started parking at the specified lot.\n\n- **RPC**: `create_parking_session` INSERTs one row into `parking_sessions` and returns the `session_id`.\n- **Idempotent**: `client_request_id` (uuid) is required — duplicate sends create only one session. Protects against network retries and double taps.\n- **GPS**: `start_lat` / `start_lng` are optional; they drive a map pin and don't need to be precise.\n- **Auth**: required. `user_id` comes from the JWT — the client's input is ignored.\n- **Next steps**: end via `POST /v1/parking-sessions/{id}/finalize`; cancel within 5 min via `/cancel`."
      }
    },
    "/v1/parking-sessions/{id}/finalize": {
      "post": {
        "summary": "Finalize parking + lock fee (RPC `finalize_parking_session`)",
        "description": "Ends a parking session and locks in the fee.\n\n- **RPC**: `finalize_parking_session` sets `end_at` and calculates `fee_amount` with the same logic as `calc_parking_fee`.\n- **Idempotent**: `client_request_id` prevents double finalization. If already finalized, returns the stored amount.\n- **Response**: `{ total_amount, breakdown }` (same shape as calc-fee). The client renders this on the receipt screen.\n- **vs cancel**: finalize commits the charge; use `/cancel` for mistaken taps within 5 min."
      }
    },
    "/v1/parking-sessions/{id}/cancel": {
      "post": {
        "summary": "Cancel parking session (RPC `cancel_parking_session`)",
        "description": "Cancels a just-started session (within 5 minutes) so no fee is charged.\n\n- **RPC**: `cancel_parking_session` moves the session to `cancelled` and snaps `end_at` close to `start_at`.\n- **Window**: beyond 5 minutes the RPC rejects — you must `finalize` instead.\n- **Idempotent**: calling on an already-cancelled session is a 200 no-op."
      }
    },
    "/v1/tags": {
      "get": { "summary": "List tag masters" }
    },
    "/v1/articles": {
      "get": { "summary": "List published articles (published only)" }
    },
    "/v1/articles/by-author/{slug}": {
      "get": { "summary": "List published articles by author slug" }
    },
    "/v1/articles/{slug}": {
      "get": { "summary": "Get article body by slug" }
    },
    "/v1/ads": {
      "get": { "summary": "List active ads (pre-filtered by period and status)" }
    },
    "/v1/support/tickets": {
      "post": { "summary": "Create support ticket" },
      "get":  { "summary": "List my support tickets" }
    },
    "/v1/error-reports": {
      "post": { "summary": "Submit an error report against parking data" }
    },
    "/v1/sponsors": {
      "get": { "summary": "List public sponsors (for map pins / web hubs)" }
    },
    "/v1/sponsors/nearby": {
      "get": {
        "summary": "Sponsors within radius of a coordinate",
        "description": "Returns publicly visible sponsor venues within the given radius, sorted by distance ASC. Used to suggest nearby deals while a user is parking.\n\n- **RPC**: `nearby_sponsors(lng, lat, radius_m)` (PostGIS), invoked via Hyperdrive.\n- **Radius**: defaults to 1500 m. `radius_m` must be a positive number.\n- **Resilience**: even if the RPC is unregistered or errors, we return 200 with an empty array (never break SSG builds or map UI).\n- **Cron**: the same RPC powers `handleSponsorProximity` every 10 minutes, sending nearby-push notifications to parking users."
      }
    },
    "/v1/sponsors/{id}/checkin": {
      "post": {
        "summary": "Check in at a sponsor venue (EXP grant happens via DB trigger)",
        "description": "Records that the authenticated user visited a sponsor venue. This is a gamification trigger (EXP / badges).\n\n- **Auth**: required. Appends a row to `sponsor_checkins`; a DB trigger updates EXP and badge progress automatically.\n- **Deduplication**: same (user, sponsor) pair is limited to one check-in per day via a DB constraint.\n- **GPS verification**: if the body includes the user's current coordinates, the server checks distance to the sponsor venue to block far-away (cheat) check-ins."
      }
    },
    "/v1/themes": {
      "get": { "summary": "List public themes" }
    },
    "/v1/subscription-plans": {
      "get": { "summary": "List subscription plans (public)" }
    },
    "/v1/storage/upload-url": {
      "post": { "summary": "Issue a Cloudflare R2 presigned PUT URL" }
    },
    "/v1/storage/assets/{id}/finalize": {
      "post": { "summary": "Commit upload completion (for metadata tweaks)" }
    },
    "/v1/search/lots": {
      "get": { "summary": "Bulk list active parking lots with pricing / attributes / tags (for SSG builds)" }
    },
    "/v1/search/ai": {
      "post": { "summary": "Parse natural-language query into parking-search filters" }
    },
    "/v1/hubs/publishable": {
      "get": { "summary": "List publishable hubs (station with inventory ≥ min)" }
    },
    "/v1/hubs/by-slug/{prefSlug}/{citySlug}/{spotSlug}": {
      "get": { "summary": "Get a single station hub by slug" }
    },
    "/v1/hubs/{stationId}/parking-lots": {
      "get": { "summary": "Parking lots near a station hub (by distance ASC)" }
    },

    "/v1/admin/admins": {
      "get":  { "summary": "List admins (paging + filter + sort)" },
      "post": { "summary": "Create a new admin (provisions the Auth user and returns the initial password)" }
    },
    "/v1/admin/admins/{id}": {
      "patch":  { "summary": "Update admin info (name / role_id / status)" },
      "delete": { "summary": "Delete admin (also removes the Auth user)" }
    },
    "/v1/admin/admins/{id}/reset-password": {
      "post": { "summary": "Reset admin password and return the new one" }
    },
    "/v1/admin/admins/me/notification-prefs": {
      "get": { "summary": "Get my (logged-in admin's) notification preferences" },
      "put": { "summary": "Update my (logged-in admin's) notification preferences" }
    },
    "/v1/admin/roles": {
      "get":  { "summary": "List roles" },
      "post": { "summary": "Create role" }
    },
    "/v1/admin/roles/{id}": {
      "patch":  { "summary": "Update role" },
      "delete": { "summary": "Delete role (disallowed when is_system=true)" }
    },
    "/v1/admin/roles/{id}/permissions": {
      "get": { "summary": "List role permissions" },
      "put": { "summary": "Replace role permission matrix (full overwrite)" }
    },
    "/v1/admin/role-permissions": {
      "get": { "summary": "All-roles × permissions map" }
    },
    "/v1/admin/parking-lots": {
      "post": { "summary": "Create parking lot" }
    },
    "/v1/admin/parking-lots/{id}": {
      "patch": { "summary": "Update parking lot" }
    },
    "/v1/admin/parking-lots/{id}/pricing-rules": {
      "put": { "summary": "Replace pricing rules in full" }
    },
    "/v1/admin/parking-lots/{id}/payment-methods": {
      "put": { "summary": "Replace accepted payment methods in full" }
    },
    "/v1/admin/parking-lots/{id}/images": {
      "post": { "summary": "Attach one image to a parking lot (by asset_id)" }
    },
    "/v1/admin/parking-lots/{id}/images/{imageId}/main": {
      "patch": { "summary": "Set image as main (un-sets any existing main)" }
    },
    "/v1/admin/parking-lots/{id}/images/{imageId}": {
      "delete": { "summary": "Delete image" }
    },
    "/v1/admin/parking-lots/{id}/tags": {
      "post": { "summary": "Attach tag" }
    },
    "/v1/admin/parking-lots/{id}/tags/{tagId}": {
      "delete": { "summary": "Detach tag" }
    },
    "/v1/admin/parking-lots/{id}/owners": {
      "get":  { "summary": "List linked owners" },
      "post": { "summary": "Link an owner" }
    },
    "/v1/admin/parking-lots/engagement-stats": {
      "get": { "summary": "Engagement stats for all parking lots (saves / sessions / search counts)" }
    },
    "/v1/admin/parking-lots/{id}/engagement-stats": {
      "get": { "summary": "Engagement stats for one parking lot" }
    },
    "/v1/admin/users": {
      "get": { "summary": "List users (paginated)" }
    },
    "/v1/admin/users/count": {
      "get": { "summary": "Total user count" }
    },
    "/v1/admin/users/options": {
      "get": { "summary": "Lightweight user list for select inputs" }
    },
    "/v1/admin/users/{id}": {
      "get": { "summary": "User detail" }
    },
    "/v1/admin/users/bulk-status": {
      "patch": { "summary": "Bulk-change status" }
    },
    "/v1/admin/users/{id}/saved-parking-lots": {
      "get": { "summary": "A user's saved parking lots" }
    },
    "/v1/admin/users/{id}/exp-progress": {
      "get": { "summary": "EXP progress (combines `user_exp` and `level_definitions`)" }
    },
    "/v1/admin/users/{id}/activity-counts": {
      "get": { "summary": "Activity counts (null if not present)" }
    },
    "/v1/admin/tags": {
      "get":  { "summary": "List tags (paginated)" },
      "post": { "summary": "Create tag" }
    },
    "/v1/admin/tags/{id}": {
      "patch":  { "summary": "Update tag" },
      "delete": { "summary": "Delete tag (CASCADE also removes `parking_lot_tags`)" }
    },
    "/v1/admin/operators": {
      "get":  { "summary": "List operators (paginated)" },
      "post": { "summary": "Create operator" }
    },
    "/v1/admin/operators/{id}": {
      "patch":  { "summary": "Update operator" },
      "delete": { "summary": "Soft-delete operator (SET NULL on linked parking lots' operator_id)" }
    },
    "/v1/admin/notifications": {
      "get":  { "summary": "List admin notifications (non-deleted + category / read filters)" },
      "post": { "summary": "Create admin-to-admin notification" }
    },
    "/v1/admin/notifications/deleted": {
      "get": { "summary": "List soft-deleted (trash)" }
    },
    "/v1/admin/notifications/{id}/read": {
      "post": { "summary": "Mark as read" }
    },
    "/v1/admin/notifications/read-all": {
      "post": { "summary": "Mark all as read" }
    },
    "/v1/admin/notifications/{id}": {
      "delete": { "summary": "Soft-delete" }
    },
    "/v1/admin/notifications/{id}/restore": {
      "post": { "summary": "Restore" }
    },
    "/v1/admin/notifications/{id}/purge": {
      "delete": { "summary": "Permanently delete" }
    },
    "/v1/admin/user-notifications": {
      "get":  { "summary": "List sent / scheduled notifications (non-deleted + status / type / title-body search)" },
      "post": { "summary": "Create a draft notification" }
    },
    "/v1/admin/user-notifications/deleted": {
      "get": { "summary": "List soft-deleted (trash)" }
    },
    "/v1/admin/user-notifications/{id}": {
      "patch":  { "summary": "Update notification" },
      "delete": { "summary": "Soft-delete" }
    },
    "/v1/admin/user-notifications/{id}/send": {
      "post": { "summary": "Enqueue FCM dispatch (async fan-out via `parky-fcm-dispatch`)" }
    },
    "/v1/admin/user-notifications/{id}/restore": {
      "post": { "summary": "Restore" }
    },
    "/v1/admin/user-notifications/{id}/purge": {
      "delete": { "summary": "Permanently delete" }
    },
    "/v1/admin/reviews": {
      "get":  { "summary": "List reviews for moderation" },
      "post": { "summary": "Create review (admin entry)" }
    },
    "/v1/admin/reviews/{id}": {
      "patch":  { "summary": "Approve / reject review or update admin note" },
      "delete": { "summary": "Delete review (policy violation, etc.)" }
    },
    "/v1/admin/support/tickets": {
      "get": { "summary": "List support tickets" }
    },
    "/v1/admin/support/tickets/{id}": {
      "get":   { "summary": "Ticket detail" },
      "patch": { "summary": "Update ticket" }
    },
    "/v1/admin/support/tickets/bulk-status": {
      "patch": { "summary": "Bulk-change status" }
    },
    "/v1/admin/error-reports": {
      "get":  { "summary": "List error reports" },
      "post": { "summary": "Create an error report (admin entry)" }
    },
    "/v1/admin/error-reports/{id}": {
      "get":   { "summary": "Get one error report (with `parking_lot` join)" },
      "patch": { "summary": "Update report" }
    },
    "/v1/admin/error-reports/bulk": {
      "patch": { "summary": "Bulk-change status or assignee" }
    },
    "/v1/admin/tasks": {
      "get": { "summary": "List tasks across domains" }
    },
    "/v1/admin/tasks/{id}": {
      "patch": { "summary": "Update task (assignee / due / urgency / memo)" }
    },
    "/v1/admin/tasks/by-ref": {
      "put": { "summary": "Upsert by ref_id (called from support / reports / applications)" }
    },
    "/v1/admin/articles": {
      "get":  { "summary": "List articles (all statuses incl. drafts + title search)" },
      "post": { "summary": "Create article" }
    },
    "/v1/admin/articles/{id}": {
      "patch":  { "summary": "Update article" },
      "delete": { "summary": "Delete article" }
    },
    "/v1/admin/ads": {
      "get":  { "summary": "List ads (admin + name search)" },
      "post": { "summary": "Create ad" }
    },
    "/v1/admin/ads/{id}": {
      "patch":  { "summary": "Update ad" },
      "delete": { "summary": "Delete ad" }
    },
    "/v1/admin/boosts": {
      "get":  { "summary": "List boosts (owner / lot / status / name search)" },
      "post": { "summary": "Create boost" }
    },
    "/v1/admin/boosts/{id}": {
      "patch": { "summary": "Update boost" }
    },
    "/v1/admin/sponsors": {
      "get":  { "summary": "List sponsors (admin + misc filters)" },
      "post": { "summary": "Create sponsor" }
    },
    "/v1/admin/sponsors/{id}": {
      "patch":  { "summary": "Update sponsor" },
      "delete": { "summary": "Soft-delete sponsor" }
    },
    "/v1/admin/owners": {
      "get":  { "summary": "List owners (paging + filter)" },
      "post": { "summary": "Create owner" }
    },
    "/v1/admin/owners/credits": {
      "get": { "summary": "List all owners' credit balances (DESC)" }
    },
    "/v1/admin/owners/{id}": {
      "patch": { "summary": "Update owner" }
    },
    "/v1/admin/owners/{id}/parking-lots": {
      "get": { "summary": "Parking lots managed by an owner" }
    },
    "/v1/admin/owners/{id}/credit": {
      "get": { "summary": "Owner credit balance" }
    },
    "/v1/admin/owners/{id}/credit-transactions": {
      "get":  { "summary": "Credit transaction history" },
      "post": { "summary": "Credit adjustment (charge / bonus)" }
    },
    "/v1/admin/owner-applications": {
      "get": { "summary": "List owner applications (paging + filter)" }
    },
    "/v1/admin/owner-applications/{id}": {
      "patch": { "summary": "Update application status" }
    },
    "/v1/admin/owner-applications/{id}/approve": {
      "post": { "summary": "Approve application (creates owner + links parking_lot_owners + updates status)" }
    },
    "/v1/admin/activity-logs": {
      "get":  { "summary": "Audit log (paginated)" },
      "post": { "summary": "Append audit log entry (admin-action record)" }
    },
    "/v1/admin/revenue/monthly": {
      "get": { "summary": "Monthly summary (view)" }
    },
    "/v1/admin/revenue/transactions": {
      "get":  { "summary": "List transactions (paginated, LEFT JOIN user / plan)" },
      "post": { "summary": "Manually add transaction" }
    },
    "/v1/admin/revenue/transactions/{id}": {
      "get": { "summary": "Transaction detail" }
    },
    "/v1/admin/revenue/aggregate": {
      "get": { "summary": "Period aggregates (total / count / average)" }
    },
    "/v1/admin/subscriptions": {
      "get": { "summary": "List subscriptions (across plans)" }
    },
    "/v1/admin/subscriptions/active-count": {
      "get": { "summary": "Active subscription count by plan" }
    },
    "/v1/admin/subscriptions/plans": {
      "get": { "summary": "List subscription plans (admin; supports `is_active` etc. filters)" }
    },
    "/v1/admin/store-integrations": {
      "get": { "summary": "List store-integration settings" }
    },
    "/v1/admin/store-sales": {
      "get": { "summary": "Daily store sales" }
    },
    "/v1/admin/store-metrics": {
      "get": { "summary": "Daily app metrics" }
    },
    "/v1/admin/store-reviews": {
      "get": { "summary": "List store reviews (optional unreplied filter)" }
    },
    "/v1/admin/store-sync-runs": {
      "get": { "summary": "Store-sync job history" }
    },
    "/v1/admin/store-sync-trigger": {
      "post": { "summary": "Manually trigger store sync (pulls sales / metrics / reviews from Play & App Store)" }
    },
    "/v1/admin/badges": {
      "get":  { "summary": "List badge definitions (filter by category / activity / active flag)" },
      "post": { "summary": "Create badge definition" }
    },
    "/v1/admin/badges/{id}": {
      "patch": { "summary": "Update badge definition" }
    },
    "/v1/admin/badges/{id}/tags": {
      "put": { "summary": "Replace badge's tag set" }
    },
    "/v1/admin/badges/{id}/progress-summary": {
      "get": { "summary": "Badge acquisition summary (total / awarded / in_progress)" }
    },
    "/v1/admin/badges/{id}/progress-users": {
      "get": { "summary": "List users with badge progress (DESC)" }
    },
    "/v1/admin/badges/{id}/backfill": {
      "post": { "summary": "Recompute progress for one badge from raw logs (RPC)" }
    },
    "/v1/admin/badges/backfill-all": {
      "post": { "summary": "Recompute progress for all badges (RPC)" }
    },
    "/v1/admin/exp-rules": {
      "get":  { "summary": "List EXP rules" },
      "post": { "summary": "Create EXP rule" }
    },
    "/v1/admin/exp-rules/{id}": {
      "patch":  { "summary": "Update EXP rule" },
      "delete": { "summary": "Delete EXP rule" }
    },
    "/v1/admin/level-definitions": {
      "get": { "summary": "List level definitions" },
      "put": { "summary": "Bulk upsert level definitions" }
    },
    "/v1/admin/user-levels/recalculate": {
      "post": { "summary": "Recalculate all user levels (RPC)" }
    },
    "/v1/admin/themes": {
      "get":  { "summary": "List themes (filter by active / free flags)" },
      "post": { "summary": "Create theme" }
    },
    "/v1/admin/themes/{id}": {
      "patch":  { "summary": "Update theme" },
      "delete": { "summary": "Delete theme" }
    },
    "/v1/admin/themes/{id}/items": {
      "put": { "summary": "Replace theme slot assignments (category → part_id)" }
    },
    "/v1/admin/themes/{id}/gift": {
      "post": { "summary": "Gift theme to users (RPC)" }
    },
    "/v1/admin/theme-parts": {
      "get":  { "summary": "List theme parts (filter by category)" },
      "post": { "summary": "Create theme part" }
    },
    "/v1/admin/theme-parts/{id}": {
      "patch":  { "summary": "Update theme part" },
      "delete": { "summary": "Delete theme part" }
    },
    "/v1/admin/theme-parts/{id}/tags": {
      "put": { "summary": "Replace a part's tag set" }
    },
    "/v1/admin/ai-providers": {
      "get": { "summary": "List AI providers" }
    },
    "/v1/admin/ai-providers/{id}": {
      "patch": { "summary": "Update AI provider configuration" }
    },
    "/v1/admin/ai-providers/{id}/register-key": {
      "post": { "summary": "Register API key into Supabase Vault (RPC `vault_insert_secret`)" }
    },
    "/v1/admin/ai-usage-logs": {
      "get": { "summary": "List AI usage logs" }
    },
    "/v1/admin/codes": {
      "get":  { "summary": "List codes for admin (includes id + is_deleted)" },
      "post": { "summary": "Create a code" }
    },
    "/v1/admin/codes/{id}": {
      "patch":  { "summary": "Update a code" },
      "delete": { "summary": "Delete a code (hard delete)" }
    },
    "/v1/admin/dashboard/metrics": {
      "get": { "summary": "Dashboard KPIs (6 metrics in one request)" }
    },
    "/v1/admin/activity-log-refs/resolve": {
      "post": { "summary": "Resolve multiple reference IDs in bulk and return display records" }
    },
    "/v1/admin/parking-sessions": {
      "get":  { "summary": "List parking sessions (LEFT JOIN user / parking lot)" },
      "post": { "summary": "Create parking session (admin on behalf of user)" }
    },
    "/v1/admin/parking-sessions/{id}": {
      "patch": { "summary": "Partially update parking session (memo / status etc.)" }
    },
    "/v1/admin/search/ai-parse": {
      "post": { "summary": "AI: convert user utterance into search filters (stubbed for now)" }
    },

    "/v1/admin/instagram/categories": {
      "get":  { "summary": "List slide categories" },
      "post": { "summary": "Create slide category" }
    },
    "/v1/admin/instagram/categories/{code}": {
      "patch":  { "summary": "Update slide category" },
      "delete": { "summary": "Delete slide category (soft)" }
    },
    "/v1/admin/instagram/post-categories": {
      "get":  { "summary": "List post categories" },
      "post": { "summary": "Create post category" }
    },
    "/v1/admin/instagram/post-categories/{code}": {
      "patch":  { "summary": "Update post category" },
      "delete": { "summary": "Delete post category (soft)" }
    },
    "/v1/admin/instagram/tags": {
      "get":  { "summary": "List tags" },
      "post": { "summary": "Create tag (returns existing if name already exists)" }
    },
    "/v1/admin/instagram/tags/{id}": {
      "patch":  { "summary": "Update tag" },
      "delete": { "summary": "Delete tag" }
    },
    "/v1/admin/instagram/posts/{id}/tags": {
      "put": { "summary": "Replace tags on a post" }
    },
    "/v1/admin/instagram/templates/{id}/tags": {
      "put": { "summary": "Replace tags on a template" }
    },
    "/v1/admin/instagram/post-templates": {
      "get":  { "summary": "List post templates" },
      "post": { "summary": "Create post template" }
    },
    "/v1/admin/instagram/post-templates/{id}": {
      "patch":  { "summary": "Update post template" },
      "delete": { "summary": "Delete post template" }
    },
    "/v1/admin/instagram/ai-providers": {
      "get": { "summary": "List AI providers available for selector (minimal fields)" }
    },
    "/v1/admin/instagram/templates/analyze-html": {
      "post": { "summary": "AI analyzes existing HTML to templatize it (`{{key}}` replacement + slot_schema)" }
    },
    "/v1/admin/instagram/templates": {
      "get":  { "summary": "List templates" },
      "post": { "summary": "Create template" }
    },
    "/v1/admin/instagram/templates/{id}": {
      "patch":  { "summary": "Update template" },
      "delete": { "summary": "Delete template" }
    },
    "/v1/admin/instagram/posts": {
      "get":  { "summary": "List campaigns" },
      "post": { "summary": "Create campaign" }
    },
    "/v1/admin/instagram/posts/{id}": {
      "get":    { "summary": "Campaign detail (includes slides + caption)" },
      "patch":  { "summary": "Update campaign" },
      "delete": { "summary": "Delete campaign (CASCADE removes slides/caption)" }
    },
    "/v1/admin/instagram/posts/{id}/slides": {
      "get":  { "summary": "List slides" },
      "post": { "summary": "Add slide" }
    },
    "/v1/admin/instagram/posts/{id}/slides/reorder": {
      "patch": { "summary": "Reorder slides" }
    },
    "/v1/admin/instagram/slides/{id}": {
      "patch":  { "summary": "Update slide content" },
      "delete": { "summary": "Delete slide" }
    },
    "/v1/admin/instagram/slides/{id}/duplicate": {
      "post": { "summary": "Duplicate slide (inserted immediately after)" }
    },
    "/v1/admin/instagram/slides/{id}/upload-png": {
      "post": { "summary": "Upload slide PNG to R2 (multipart/form-data)" }
    },
    "/v1/admin/instagram/upload-image": {
      "post": { "summary": "Upload content image to R2 (multipart/form-data)" }
    },
    "/v1/admin/instagram/slides/{id}/confirm-png": {
      "patch": { "summary": "Record PNG upload completion" }
    },
    "/v1/admin/instagram/slides/{id}/generate-content": {
      "post": { "summary": "LLM auto-generates content following the slide's slot_schema" }
    },
    "/v1/admin/instagram/posts/{id}/generate-all": {
      "post": { "summary": "AI assembles the whole post from the template set" }
    },
    "/v1/admin/instagram/slides/{id}/revise": {
      "post": { "summary": "LLM-driven slide HTML revision" }
    },
    "/v1/admin/instagram/templates/{id}/revise": {
      "post": { "summary": "LLM-driven template HTML revision (preview only, not saved)" }
    },
    "/v1/admin/instagram/posts/{id}/generate-caption": {
      "post": { "summary": "LLM generates the caption" }
    },
    "/v1/admin/instagram/posts/{id}/generate-ideas": {
      "post": { "summary": "Competitive analysis and content-idea generation" }
    },
    "/v1/admin/instagram/posts/{id}/caption": {
      "patch": { "summary": "Manually update caption" }
    },
    "/v1/admin/instagram/images/detect-sensitive": {
      "post": { "summary": "Detect face / license-plate candidate regions in an image" }
    },
    "/v1/admin/instagram/posts/{id}/snapshots": {
      "get": { "summary": "List competitive-analysis snapshots" }
    },

    "/v1/admin/assets": {
      "get":  { "summary": "List assets (category filter + paging)" },
      "post": { "summary": "Create asset (metadata only; upload the file to R2 separately)" }
    },
    "/v1/admin/assets/{id}": {
      "get":    { "summary": "Asset detail" },
      "patch":  { "summary": "Update asset" },
      "delete": { "summary": "Delete asset" }
    },
    "/v1/admin/user-vehicles": {
      "get":  { "summary": "List all users' vehicles (LEFT JOIN user / asset)" },
      "post": { "summary": "Register vehicle (admin on behalf of user)" }
    },
    "/v1/admin/user-vehicles/{id}": {
      "get":    { "summary": "Get one vehicle (with user / asset join)" },
      "patch":  { "summary": "Partially update vehicle (toggling `is_primary` un-sets the others)" },
      "delete": { "summary": "Soft-delete vehicle (`deleted_at=NOW()`)" }
    }
  }
}
