{
  "openapi": "3.0.0",
  "paths": {
    "/": {
      "get": {
        "operationId": "AppController_redirectToDocs",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Root endpoint - redirects to API docs",
        "tags": [
          "App"
        ]
      }
    },
    "/health": {
      "get": {
        "operationId": "AppController_healthCheck",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Service is healthy"
          }
        },
        "summary": "Health check",
        "tags": [
          "App"
        ]
      }
    },
    "/projects": {
      "get": {
        "operationId": "ProjectsController_getProjects",
        "parameters": [
          {
            "name": "scheduledAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scheduledBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "deliveredAfter",
            "required": false,
            "in": "query",
            "description": "Delivery-date range, filtering `Project.deliveryEnabledAt`.\n\nIMPORTANT: `deliveryEnabledAt` is the LATEST delivery — it is overwritten\nevery time the project is delivered again (revision re-deliveries,\nre-sends), so this filter means \"most recently delivered in this range\",\nnot \"was ever delivered in this range\". A project delivered in March and\nre-delivered in May does NOT match a March range.\n\nA bare `YYYY-MM-DD` covers the whole UTC day on both ends, so\nafter = before = one day returns that day's deliveries.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "deliveredBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "firstDeliveredAfter",
            "required": false,
            "in": "query",
            "description": "FIRST-delivery range, filtering `Project.firstDeliveredAt` — written once,\nthe first time the job was delivered, and never overwritten.\n\nThis is the counterpart to deliveredAfter/deliveredBefore above, which read\n`deliveryEnabledAt` (the LATEST delivery). Both exist because both questions\nare real and they give different answers whenever a job is re-delivered:\n\"what did we deliver in March\" wants the first delivery, \"what is live /\nmost recently delivered\" wants the latest. A job first delivered in March\nand re-delivered in July is in a March range here, and in a July range\nthere.\n\nSame bare-date semantics as every other range: a `YYYY-MM-DD` covers the\nwhole UTC day on both ends, so after = before = one day returns that day.\n\nThe column is NULL for rows delivered before it existed, so this range only\nsees deliveries actually on record — it never infers one from\n`deliveryEnabledAt`, which would re-import exactly the ambiguity it exists\nto remove.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "firstDeliveredBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Object"
              }
            }
          },
          {
            "name": "paymentStatus",
            "required": false,
            "in": "query",
            "description": "Payment status filter: 'paid' → paidAt set; 'unpaid' → paidAt null.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "customerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianId",
            "required": false,
            "in": "query",
            "description": "Single technician (saved views + deep links keep sending this). Merged\nserver-side with `technicianIds`, deduped — the two are one filter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianIds",
            "required": false,
            "in": "query",
            "description": "Multi-select technicians. Matches a person assigned ANYWHERE on the\nproject: the legacy `Project.technicianId` scalar, the ProjectTechnician\njoin, a shoot's own technician, or a shoot's crew.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "technicianMatch",
            "required": false,
            "in": "query",
            "description": "How to combine `technicianIds`:\n  - 'any' (default) → the project has at least one of them assigned\n  - 'all'           → EVERY selected person is assigned somewhere on the\n                      project. Deliberately not \"all on the same shoot\" —\n                      crews are per-visit, and the useful question when\n                      auditing a job is who worked it at all.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "crewSize",
            "required": false,
            "in": "query",
            "description": "Shoot-crew size — how many DISTINCT technicians are staffed on the job,\ncounted across all four places one can be attached (the project scalar,\nProjectTechnician, a LIVE shoot's own technician, and that shoot's crew).\n\n  'solo' | '1'              → exactly one technician\n  'crew' | 'team' | 'multi' → two or more\n  'N' | 'N+' (N = 1..50)    → N or more\n  '' (a cleared select)     → no filter\n\nUnassigned jobs (0 technicians) match NO crew size — that question is\n`technicianAssigned=false`.\n\nThe pattern deliberately mirrors ProjectsService.parseCrewSizeFilter, which\nowns this vocabulary: `whitelist: true` strips anything this DTO does not\ndeclare, so a value the parser understands but this regex rejects would be\nunreachable code, while anything this lets through that the parser does not\naccept (N outside 1..50) gets a 400 from the parser itself. Keep the two in\nstep.\n\nNOT applied by buildProjectFilters. Prisma cannot express a to-many\ncardinality in `where` (no `_count`, no self-correlation), so this resolves\nthrough a pre-query that is AND-ed onto the clause — and it has to happen on\nthe list, the ids (\"select all\") AND the metrics query, or the chip count\ndisagrees with the rows beneath it. ProjectsService.applyCrewSizeFilter is\nthe one place that does it, and every project-list surface routes through\nit.",
            "schema": {
              "pattern": "^(solo|crew|team|multi|\\d{1,2}\\+?)?$",
              "type": "string"
            }
          },
          {
            "name": "serviceAreaIds",
            "required": false,
            "in": "query",
            "description": "Service-area coverage, matched against the DERIVED\n`Project.resolvedServiceAreaId` (where the property falls TODAY, re-derived\nfrom lat/lng against the org's current areas) plus two sentinels for the\nstates that have no id:\n  - '__outside__' → placed, and inside no area\n  - '__unknown__' → could not be placed at all (no usable coordinates)\nThey are not uuids, so they cannot collide with a real area id. See\nSERVICE_AREA_OUTSIDE / SERVICE_AREA_UNKNOWN in common/utils/filter-builder.\n\nThis filter existed as a correct predicate for a while but was withheld\nfrom the DTO, because it pointed at the frozen `Project.serviceAreaId` —\na booking-time BILLING snapshot that InvoicesService reads to choose\nper-area pricing, and that carries a value on ~no rows (0 of 3218 on dev).\nFiltering it matched nothing, and backfilling it would have changed what\nun-invoiced jobs cost. The derived column exists so filtering can be right\nwithout money moving; the frozen column stays frozen.\n\n'__unknown__' is always trustworthy. '__outside__' is not: NULL covers both\n\"placed, matched nothing\" and \"nothing was derived\", so it needs the\nbackfill to have run AND the org to have active coordinate-based coverage\n(an org with name-based areas only resolves nothing, and every project would\nread as outside). See the predicate comment in buildProjectFilters.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "travelFeeZoneIds",
            "required": false,
            "in": "query",
            "description": "Travel-fee zone, matched against the DERIVED\n`Project.resolvedTravelFeeZoneId`. Zones are the concentric distance bands\nconfigured inside each service area (they live only in the\n`Organization.serviceArea` JSON — there is no zone table), so a zone id is\nonly meaningful next to its area; pair this with `serviceAreaIds`.\n\nWithheld until now for the same reason as serviceAreaIds: the\n`Project.travelFee*` columns are the immutable billing snapshot, frozen so\nthat moving a zone boundary can never rewrite a historical invoice, and 0%\npopulated. \"Which zone is this property in today\" is a different question,\nand it gets its own derived column.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "editorId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pipelineStageId",
            "required": false,
            "in": "query",
            "description": "Kanban stage membership filter (see ProjectFilterParams.pipelineStageId).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "projectManagerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianAssigned",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "importSource",
            "required": false,
            "in": "query",
            "description": "Filter by import provenance (Project.importSource) — e.g. 'aryeo' to select\nonly Aryeo-imported projects. Lets \"select all across pages\" resolve the\nAryeo-only subset server-side (the loaded page can't be filtered for the\noff-page rows client-side, since importSource only rides on loaded cards).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "schedulingStatus",
            "required": false,
            "in": "query",
            "description": "Partition the list by scheduling state:\n  - 'pending'   → only projects with no shoot time yet (scheduledTime null)\n  - 'scheduled' → only projects that have a shoot time (scheduledTime set)\nOmitted → both are returned (legacy behaviour). The company Projects\npage splits into a \"scheduling pending\" carousel ('pending') and the\nmain paginated grid ('scheduled') so the grid's page count reflects\nscheduled projects only.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "view",
            "required": false,
            "in": "query",
            "description": "Response shape. `card` returns a LEAN per-project payload with only the\nrelations a list/board card renders (dropping the deep joins the detail\nview uses), which massively cuts the query + JSON for card grids like the\nkanban board. Omitted/`full` keeps the historic fat include. Consumers that\npass `card` must hydrate full detail separately (e.g. on opening a job).",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "packageId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mediaTypes",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "addOnIds",
            "required": false,
            "in": "query",
            "description": "Add-ons selected at booking (`Project.selectedAddOnIds`). Any-of: a\nproject matches when it carries at least one of the selected add-ons.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "includeArchived",
            "required": false,
            "in": "query",
            "description": "When true, archived projects are included in the result. Defaults to\nfalse (archived projects are hidden). Only the deep \"Archived Orders\"\nsettings page should pass true.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "archivedOnly",
            "required": false,
            "in": "query",
            "description": "When true, returns ONLY archived orders — the projects-page \"Archived\"\nfilter. Distinct from includeArchived (which returns both). Also opts out\nof the default cancelled-exclusion since archived orders are cancelled.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "workspaceOrgId",
            "required": false,
            "in": "query",
            "description": "For AGENT users only: when set, scopes the cross-org \"my projects\"\nlist to a specific workspace.\n- If the org is COMPANY/TEAM: returns only projects fulfilled by that org.\n- If the org is the agent's PERSONAL org: returns only projects fulfilled\n  by solo providers (PERSONAL orgs).\n- Otherwise: ignored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List all projects in organization",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/status-counts": {
      "get": {
        "operationId": "ProjectsController_getStatusCounts",
        "parameters": [
          {
            "name": "search",
            "required": false,
            "in": "query",
            "description": "Server-side search (address + customer name).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "customerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianIds",
            "required": false,
            "in": "query",
            "description": "Multi-select technicians, merged + deduped with `technicianId` server-side.\nWithout it the badges silently went org-wide the moment a SECOND technician\nwas picked: the list sends `technicianIds` for 2+ selections and only\n`technicianId` for one, so a two-person filter left the counts unfiltered\nabove filtered cards — the exact drift this DTO exists to prevent.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "technicianMatch",
            "required": false,
            "in": "query",
            "description": "'any' (default) or 'all'. See ListProjectsDto.technicianMatch.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "editorId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "projectManagerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "packageId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "paymentStatus",
            "required": false,
            "in": "query",
            "description": "'paid' → paidAt set; 'unpaid' → paidAt null.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "mediaTypes",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "scheduledAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scheduledBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "addOnIds",
            "required": false,
            "in": "query",
            "description": "Add-ons picked at booking (any-of). Same `hasSome` predicate as the list.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "deliveredAfter",
            "required": false,
            "in": "query",
            "description": "Latest-delivery range (`deliveryEnabledAt`).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "deliveredBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "firstDeliveredAfter",
            "required": false,
            "in": "query",
            "description": "First-delivery range (`firstDeliveredAt`, write-once).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "firstDeliveredBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "serviceAreaIds",
            "required": false,
            "in": "query",
            "description": "Coverage family, against the DERIVED columns (never the frozen billing\nsnapshot). Accepted here so the board's badges agree with its cards when a\ncoverage filter is on; `crewSize` deliberately still is NOT — it needs the\npre-query in applyCrewSizeFilter and buildProjectFilters 400s on it, so the\nhonest move is to reject the field rather than accept and ignore it.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "travelFeeZoneIds",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get project counts by status",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/filter-options/service-areas": {
      "get": {
        "operationId": "ProjectsController_getServiceAreaFilterOptions",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Service areas + travel fee zones for the projects filter bar",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/filter-options/assignees": {
      "get": {
        "operationId": "ProjectsController_getAssigneeFilterOptions",
        "parameters": [
          {
            "name": "scheduledAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scheduledBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "deliveredAfter",
            "required": false,
            "in": "query",
            "description": "Delivery-date range, filtering `Project.deliveryEnabledAt`.\n\nIMPORTANT: `deliveryEnabledAt` is the LATEST delivery — it is overwritten\nevery time the project is delivered again (revision re-deliveries,\nre-sends), so this filter means \"most recently delivered in this range\",\nnot \"was ever delivered in this range\". A project delivered in March and\nre-delivered in May does NOT match a March range.\n\nA bare `YYYY-MM-DD` covers the whole UTC day on both ends, so\nafter = before = one day returns that day's deliveries.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "deliveredBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "firstDeliveredAfter",
            "required": false,
            "in": "query",
            "description": "FIRST-delivery range, filtering `Project.firstDeliveredAt` — written once,\nthe first time the job was delivered, and never overwritten.\n\nThis is the counterpart to deliveredAfter/deliveredBefore above, which read\n`deliveryEnabledAt` (the LATEST delivery). Both exist because both questions\nare real and they give different answers whenever a job is re-delivered:\n\"what did we deliver in March\" wants the first delivery, \"what is live /\nmost recently delivered\" wants the latest. A job first delivered in March\nand re-delivered in July is in a March range here, and in a July range\nthere.\n\nSame bare-date semantics as every other range: a `YYYY-MM-DD` covers the\nwhole UTC day on both ends, so after = before = one day returns that day.\n\nThe column is NULL for rows delivered before it existed, so this range only\nsees deliveries actually on record — it never infers one from\n`deliveryEnabledAt`, which would re-import exactly the ambiguity it exists\nto remove.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "firstDeliveredBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Object"
              }
            }
          },
          {
            "name": "paymentStatus",
            "required": false,
            "in": "query",
            "description": "Payment status filter: 'paid' → paidAt set; 'unpaid' → paidAt null.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "customerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianId",
            "required": false,
            "in": "query",
            "description": "Single technician (saved views + deep links keep sending this). Merged\nserver-side with `technicianIds`, deduped — the two are one filter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianIds",
            "required": false,
            "in": "query",
            "description": "Multi-select technicians. Matches a person assigned ANYWHERE on the\nproject: the legacy `Project.technicianId` scalar, the ProjectTechnician\njoin, a shoot's own technician, or a shoot's crew.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "technicianMatch",
            "required": false,
            "in": "query",
            "description": "How to combine `technicianIds`:\n  - 'any' (default) → the project has at least one of them assigned\n  - 'all'           → EVERY selected person is assigned somewhere on the\n                      project. Deliberately not \"all on the same shoot\" —\n                      crews are per-visit, and the useful question when\n                      auditing a job is who worked it at all.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "crewSize",
            "required": false,
            "in": "query",
            "description": "Shoot-crew size — how many DISTINCT technicians are staffed on the job,\ncounted across all four places one can be attached (the project scalar,\nProjectTechnician, a LIVE shoot's own technician, and that shoot's crew).\n\n  'solo' | '1'              → exactly one technician\n  'crew' | 'team' | 'multi' → two or more\n  'N' | 'N+' (N = 1..50)    → N or more\n  '' (a cleared select)     → no filter\n\nUnassigned jobs (0 technicians) match NO crew size — that question is\n`technicianAssigned=false`.\n\nThe pattern deliberately mirrors ProjectsService.parseCrewSizeFilter, which\nowns this vocabulary: `whitelist: true` strips anything this DTO does not\ndeclare, so a value the parser understands but this regex rejects would be\nunreachable code, while anything this lets through that the parser does not\naccept (N outside 1..50) gets a 400 from the parser itself. Keep the two in\nstep.\n\nNOT applied by buildProjectFilters. Prisma cannot express a to-many\ncardinality in `where` (no `_count`, no self-correlation), so this resolves\nthrough a pre-query that is AND-ed onto the clause — and it has to happen on\nthe list, the ids (\"select all\") AND the metrics query, or the chip count\ndisagrees with the rows beneath it. ProjectsService.applyCrewSizeFilter is\nthe one place that does it, and every project-list surface routes through\nit.",
            "schema": {
              "pattern": "^(solo|crew|team|multi|\\d{1,2}\\+?)?$",
              "type": "string"
            }
          },
          {
            "name": "serviceAreaIds",
            "required": false,
            "in": "query",
            "description": "Service-area coverage, matched against the DERIVED\n`Project.resolvedServiceAreaId` (where the property falls TODAY, re-derived\nfrom lat/lng against the org's current areas) plus two sentinels for the\nstates that have no id:\n  - '__outside__' → placed, and inside no area\n  - '__unknown__' → could not be placed at all (no usable coordinates)\nThey are not uuids, so they cannot collide with a real area id. See\nSERVICE_AREA_OUTSIDE / SERVICE_AREA_UNKNOWN in common/utils/filter-builder.\n\nThis filter existed as a correct predicate for a while but was withheld\nfrom the DTO, because it pointed at the frozen `Project.serviceAreaId` —\na booking-time BILLING snapshot that InvoicesService reads to choose\nper-area pricing, and that carries a value on ~no rows (0 of 3218 on dev).\nFiltering it matched nothing, and backfilling it would have changed what\nun-invoiced jobs cost. The derived column exists so filtering can be right\nwithout money moving; the frozen column stays frozen.\n\n'__unknown__' is always trustworthy. '__outside__' is not: NULL covers both\n\"placed, matched nothing\" and \"nothing was derived\", so it needs the\nbackfill to have run AND the org to have active coordinate-based coverage\n(an org with name-based areas only resolves nothing, and every project would\nread as outside). See the predicate comment in buildProjectFilters.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "travelFeeZoneIds",
            "required": false,
            "in": "query",
            "description": "Travel-fee zone, matched against the DERIVED\n`Project.resolvedTravelFeeZoneId`. Zones are the concentric distance bands\nconfigured inside each service area (they live only in the\n`Organization.serviceArea` JSON — there is no zone table), so a zone id is\nonly meaningful next to its area; pair this with `serviceAreaIds`.\n\nWithheld until now for the same reason as serviceAreaIds: the\n`Project.travelFee*` columns are the immutable billing snapshot, frozen so\nthat moving a zone boundary can never rewrite a historical invoice, and 0%\npopulated. \"Which zone is this property in today\" is a different question,\nand it gets its own derived column.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "editorId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pipelineStageId",
            "required": false,
            "in": "query",
            "description": "Kanban stage membership filter (see ProjectFilterParams.pipelineStageId).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "projectManagerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianAssigned",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "importSource",
            "required": false,
            "in": "query",
            "description": "Filter by import provenance (Project.importSource) — e.g. 'aryeo' to select\nonly Aryeo-imported projects. Lets \"select all across pages\" resolve the\nAryeo-only subset server-side (the loaded page can't be filtered for the\noff-page rows client-side, since importSource only rides on loaded cards).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "schedulingStatus",
            "required": false,
            "in": "query",
            "description": "Partition the list by scheduling state:\n  - 'pending'   → only projects with no shoot time yet (scheduledTime null)\n  - 'scheduled' → only projects that have a shoot time (scheduledTime set)\nOmitted → both are returned (legacy behaviour). The company Projects\npage splits into a \"scheduling pending\" carousel ('pending') and the\nmain paginated grid ('scheduled') so the grid's page count reflects\nscheduled projects only.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "view",
            "required": false,
            "in": "query",
            "description": "Response shape. `card` returns a LEAN per-project payload with only the\nrelations a list/board card renders (dropping the deep joins the detail\nview uses), which massively cuts the query + JSON for card grids like the\nkanban board. Omitted/`full` keeps the historic fat include. Consumers that\npass `card` must hydrate full detail separately (e.g. on opening a job).",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "packageId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mediaTypes",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "addOnIds",
            "required": false,
            "in": "query",
            "description": "Add-ons selected at booking (`Project.selectedAddOnIds`). Any-of: a\nproject matches when it carries at least one of the selected add-ons.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "includeArchived",
            "required": false,
            "in": "query",
            "description": "When true, archived projects are included in the result. Defaults to\nfalse (archived projects are hidden). Only the deep \"Archived Orders\"\nsettings page should pass true.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "archivedOnly",
            "required": false,
            "in": "query",
            "description": "When true, returns ONLY archived orders — the projects-page \"Archived\"\nfilter. Distinct from includeArchived (which returns both). Also opts out\nof the default cancelled-exclusion since archived orders are cancelled.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "workspaceOrgId",
            "required": false,
            "in": "query",
            "description": "For AGENT users only: when set, scopes the cross-org \"my projects\"\nlist to a specific workspace.\n- If the org is COMPANY/TEAM: returns only projects fulfilled by that org.\n- If the org is the agent's PERSONAL org: returns only projects fulfilled\n  by solo providers (PERSONAL orgs).\n- Otherwise: ignored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Technicians / editors / project managers actually assigned on a project, for the projects filter bar",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/ids": {
      "get": {
        "operationId": "ProjectsController_getProjectIds",
        "parameters": [
          {
            "name": "scheduledAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scheduledBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "deliveredAfter",
            "required": false,
            "in": "query",
            "description": "Delivery-date range, filtering `Project.deliveryEnabledAt`.\n\nIMPORTANT: `deliveryEnabledAt` is the LATEST delivery — it is overwritten\nevery time the project is delivered again (revision re-deliveries,\nre-sends), so this filter means \"most recently delivered in this range\",\nnot \"was ever delivered in this range\". A project delivered in March and\nre-delivered in May does NOT match a March range.\n\nA bare `YYYY-MM-DD` covers the whole UTC day on both ends, so\nafter = before = one day returns that day's deliveries.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "deliveredBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "firstDeliveredAfter",
            "required": false,
            "in": "query",
            "description": "FIRST-delivery range, filtering `Project.firstDeliveredAt` — written once,\nthe first time the job was delivered, and never overwritten.\n\nThis is the counterpart to deliveredAfter/deliveredBefore above, which read\n`deliveryEnabledAt` (the LATEST delivery). Both exist because both questions\nare real and they give different answers whenever a job is re-delivered:\n\"what did we deliver in March\" wants the first delivery, \"what is live /\nmost recently delivered\" wants the latest. A job first delivered in March\nand re-delivered in July is in a March range here, and in a July range\nthere.\n\nSame bare-date semantics as every other range: a `YYYY-MM-DD` covers the\nwhole UTC day on both ends, so after = before = one day returns that day.\n\nThe column is NULL for rows delivered before it existed, so this range only\nsees deliveries actually on record — it never infers one from\n`deliveryEnabledAt`, which would re-import exactly the ambiguity it exists\nto remove.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "firstDeliveredBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Object"
              }
            }
          },
          {
            "name": "paymentStatus",
            "required": false,
            "in": "query",
            "description": "Payment status filter: 'paid' → paidAt set; 'unpaid' → paidAt null.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "customerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianId",
            "required": false,
            "in": "query",
            "description": "Single technician (saved views + deep links keep sending this). Merged\nserver-side with `technicianIds`, deduped — the two are one filter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianIds",
            "required": false,
            "in": "query",
            "description": "Multi-select technicians. Matches a person assigned ANYWHERE on the\nproject: the legacy `Project.technicianId` scalar, the ProjectTechnician\njoin, a shoot's own technician, or a shoot's crew.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "technicianMatch",
            "required": false,
            "in": "query",
            "description": "How to combine `technicianIds`:\n  - 'any' (default) → the project has at least one of them assigned\n  - 'all'           → EVERY selected person is assigned somewhere on the\n                      project. Deliberately not \"all on the same shoot\" —\n                      crews are per-visit, and the useful question when\n                      auditing a job is who worked it at all.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "crewSize",
            "required": false,
            "in": "query",
            "description": "Shoot-crew size — how many DISTINCT technicians are staffed on the job,\ncounted across all four places one can be attached (the project scalar,\nProjectTechnician, a LIVE shoot's own technician, and that shoot's crew).\n\n  'solo' | '1'              → exactly one technician\n  'crew' | 'team' | 'multi' → two or more\n  'N' | 'N+' (N = 1..50)    → N or more\n  '' (a cleared select)     → no filter\n\nUnassigned jobs (0 technicians) match NO crew size — that question is\n`technicianAssigned=false`.\n\nThe pattern deliberately mirrors ProjectsService.parseCrewSizeFilter, which\nowns this vocabulary: `whitelist: true` strips anything this DTO does not\ndeclare, so a value the parser understands but this regex rejects would be\nunreachable code, while anything this lets through that the parser does not\naccept (N outside 1..50) gets a 400 from the parser itself. Keep the two in\nstep.\n\nNOT applied by buildProjectFilters. Prisma cannot express a to-many\ncardinality in `where` (no `_count`, no self-correlation), so this resolves\nthrough a pre-query that is AND-ed onto the clause — and it has to happen on\nthe list, the ids (\"select all\") AND the metrics query, or the chip count\ndisagrees with the rows beneath it. ProjectsService.applyCrewSizeFilter is\nthe one place that does it, and every project-list surface routes through\nit.",
            "schema": {
              "pattern": "^(solo|crew|team|multi|\\d{1,2}\\+?)?$",
              "type": "string"
            }
          },
          {
            "name": "serviceAreaIds",
            "required": false,
            "in": "query",
            "description": "Service-area coverage, matched against the DERIVED\n`Project.resolvedServiceAreaId` (where the property falls TODAY, re-derived\nfrom lat/lng against the org's current areas) plus two sentinels for the\nstates that have no id:\n  - '__outside__' → placed, and inside no area\n  - '__unknown__' → could not be placed at all (no usable coordinates)\nThey are not uuids, so they cannot collide with a real area id. See\nSERVICE_AREA_OUTSIDE / SERVICE_AREA_UNKNOWN in common/utils/filter-builder.\n\nThis filter existed as a correct predicate for a while but was withheld\nfrom the DTO, because it pointed at the frozen `Project.serviceAreaId` —\na booking-time BILLING snapshot that InvoicesService reads to choose\nper-area pricing, and that carries a value on ~no rows (0 of 3218 on dev).\nFiltering it matched nothing, and backfilling it would have changed what\nun-invoiced jobs cost. The derived column exists so filtering can be right\nwithout money moving; the frozen column stays frozen.\n\n'__unknown__' is always trustworthy. '__outside__' is not: NULL covers both\n\"placed, matched nothing\" and \"nothing was derived\", so it needs the\nbackfill to have run AND the org to have active coordinate-based coverage\n(an org with name-based areas only resolves nothing, and every project would\nread as outside). See the predicate comment in buildProjectFilters.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "travelFeeZoneIds",
            "required": false,
            "in": "query",
            "description": "Travel-fee zone, matched against the DERIVED\n`Project.resolvedTravelFeeZoneId`. Zones are the concentric distance bands\nconfigured inside each service area (they live only in the\n`Organization.serviceArea` JSON — there is no zone table), so a zone id is\nonly meaningful next to its area; pair this with `serviceAreaIds`.\n\nWithheld until now for the same reason as serviceAreaIds: the\n`Project.travelFee*` columns are the immutable billing snapshot, frozen so\nthat moving a zone boundary can never rewrite a historical invoice, and 0%\npopulated. \"Which zone is this property in today\" is a different question,\nand it gets its own derived column.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "editorId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pipelineStageId",
            "required": false,
            "in": "query",
            "description": "Kanban stage membership filter (see ProjectFilterParams.pipelineStageId).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "projectManagerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianAssigned",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "importSource",
            "required": false,
            "in": "query",
            "description": "Filter by import provenance (Project.importSource) — e.g. 'aryeo' to select\nonly Aryeo-imported projects. Lets \"select all across pages\" resolve the\nAryeo-only subset server-side (the loaded page can't be filtered for the\noff-page rows client-side, since importSource only rides on loaded cards).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "schedulingStatus",
            "required": false,
            "in": "query",
            "description": "Partition the list by scheduling state:\n  - 'pending'   → only projects with no shoot time yet (scheduledTime null)\n  - 'scheduled' → only projects that have a shoot time (scheduledTime set)\nOmitted → both are returned (legacy behaviour). The company Projects\npage splits into a \"scheduling pending\" carousel ('pending') and the\nmain paginated grid ('scheduled') so the grid's page count reflects\nscheduled projects only.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "view",
            "required": false,
            "in": "query",
            "description": "Response shape. `card` returns a LEAN per-project payload with only the\nrelations a list/board card renders (dropping the deep joins the detail\nview uses), which massively cuts the query + JSON for card grids like the\nkanban board. Omitted/`full` keeps the historic fat include. Consumers that\npass `card` must hydrate full detail separately (e.g. on opening a job).",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "packageId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mediaTypes",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "addOnIds",
            "required": false,
            "in": "query",
            "description": "Add-ons selected at booking (`Project.selectedAddOnIds`). Any-of: a\nproject matches when it carries at least one of the selected add-ons.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "includeArchived",
            "required": false,
            "in": "query",
            "description": "When true, archived projects are included in the result. Defaults to\nfalse (archived projects are hidden). Only the deep \"Archived Orders\"\nsettings page should pass true.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "archivedOnly",
            "required": false,
            "in": "query",
            "description": "When true, returns ONLY archived orders — the projects-page \"Archived\"\nfilter. Distinct from includeArchived (which returns both). Also opts out\nof the default cancelled-exclusion since archived orders are cancelled.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "workspaceOrgId",
            "required": false,
            "in": "query",
            "description": "For AGENT users only: when set, scopes the cross-org \"my projects\"\nlist to a specific workspace.\n- If the org is COMPANY/TEAM: returns only projects fulfilled by that org.\n- If the org is the agent's PERSONAL org: returns only projects fulfilled\n  by solo providers (PERSONAL orgs).\n- Otherwise: ignored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get all project IDs matching filters",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/metrics": {
      "get": {
        "operationId": "ProjectsController_getProjectMetrics",
        "parameters": [
          {
            "name": "scheduledAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scheduledBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "deliveredAfter",
            "required": false,
            "in": "query",
            "description": "Delivery-date range, filtering `Project.deliveryEnabledAt`.\n\nIMPORTANT: `deliveryEnabledAt` is the LATEST delivery — it is overwritten\nevery time the project is delivered again (revision re-deliveries,\nre-sends), so this filter means \"most recently delivered in this range\",\nnot \"was ever delivered in this range\". A project delivered in March and\nre-delivered in May does NOT match a March range.\n\nA bare `YYYY-MM-DD` covers the whole UTC day on both ends, so\nafter = before = one day returns that day's deliveries.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "deliveredBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "firstDeliveredAfter",
            "required": false,
            "in": "query",
            "description": "FIRST-delivery range, filtering `Project.firstDeliveredAt` — written once,\nthe first time the job was delivered, and never overwritten.\n\nThis is the counterpart to deliveredAfter/deliveredBefore above, which read\n`deliveryEnabledAt` (the LATEST delivery). Both exist because both questions\nare real and they give different answers whenever a job is re-delivered:\n\"what did we deliver in March\" wants the first delivery, \"what is live /\nmost recently delivered\" wants the latest. A job first delivered in March\nand re-delivered in July is in a March range here, and in a July range\nthere.\n\nSame bare-date semantics as every other range: a `YYYY-MM-DD` covers the\nwhole UTC day on both ends, so after = before = one day returns that day.\n\nThe column is NULL for rows delivered before it existed, so this range only\nsees deliveries actually on record — it never infers one from\n`deliveryEnabledAt`, which would re-import exactly the ambiguity it exists\nto remove.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "firstDeliveredBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Object"
              }
            }
          },
          {
            "name": "paymentStatus",
            "required": false,
            "in": "query",
            "description": "Payment status filter: 'paid' → paidAt set; 'unpaid' → paidAt null.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "customerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianId",
            "required": false,
            "in": "query",
            "description": "Single technician (saved views + deep links keep sending this). Merged\nserver-side with `technicianIds`, deduped — the two are one filter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianIds",
            "required": false,
            "in": "query",
            "description": "Multi-select technicians. Matches a person assigned ANYWHERE on the\nproject: the legacy `Project.technicianId` scalar, the ProjectTechnician\njoin, a shoot's own technician, or a shoot's crew.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "technicianMatch",
            "required": false,
            "in": "query",
            "description": "How to combine `technicianIds`:\n  - 'any' (default) → the project has at least one of them assigned\n  - 'all'           → EVERY selected person is assigned somewhere on the\n                      project. Deliberately not \"all on the same shoot\" —\n                      crews are per-visit, and the useful question when\n                      auditing a job is who worked it at all.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "crewSize",
            "required": false,
            "in": "query",
            "description": "Shoot-crew size — how many DISTINCT technicians are staffed on the job,\ncounted across all four places one can be attached (the project scalar,\nProjectTechnician, a LIVE shoot's own technician, and that shoot's crew).\n\n  'solo' | '1'              → exactly one technician\n  'crew' | 'team' | 'multi' → two or more\n  'N' | 'N+' (N = 1..50)    → N or more\n  '' (a cleared select)     → no filter\n\nUnassigned jobs (0 technicians) match NO crew size — that question is\n`technicianAssigned=false`.\n\nThe pattern deliberately mirrors ProjectsService.parseCrewSizeFilter, which\nowns this vocabulary: `whitelist: true` strips anything this DTO does not\ndeclare, so a value the parser understands but this regex rejects would be\nunreachable code, while anything this lets through that the parser does not\naccept (N outside 1..50) gets a 400 from the parser itself. Keep the two in\nstep.\n\nNOT applied by buildProjectFilters. Prisma cannot express a to-many\ncardinality in `where` (no `_count`, no self-correlation), so this resolves\nthrough a pre-query that is AND-ed onto the clause — and it has to happen on\nthe list, the ids (\"select all\") AND the metrics query, or the chip count\ndisagrees with the rows beneath it. ProjectsService.applyCrewSizeFilter is\nthe one place that does it, and every project-list surface routes through\nit.",
            "schema": {
              "pattern": "^(solo|crew|team|multi|\\d{1,2}\\+?)?$",
              "type": "string"
            }
          },
          {
            "name": "serviceAreaIds",
            "required": false,
            "in": "query",
            "description": "Service-area coverage, matched against the DERIVED\n`Project.resolvedServiceAreaId` (where the property falls TODAY, re-derived\nfrom lat/lng against the org's current areas) plus two sentinels for the\nstates that have no id:\n  - '__outside__' → placed, and inside no area\n  - '__unknown__' → could not be placed at all (no usable coordinates)\nThey are not uuids, so they cannot collide with a real area id. See\nSERVICE_AREA_OUTSIDE / SERVICE_AREA_UNKNOWN in common/utils/filter-builder.\n\nThis filter existed as a correct predicate for a while but was withheld\nfrom the DTO, because it pointed at the frozen `Project.serviceAreaId` —\na booking-time BILLING snapshot that InvoicesService reads to choose\nper-area pricing, and that carries a value on ~no rows (0 of 3218 on dev).\nFiltering it matched nothing, and backfilling it would have changed what\nun-invoiced jobs cost. The derived column exists so filtering can be right\nwithout money moving; the frozen column stays frozen.\n\n'__unknown__' is always trustworthy. '__outside__' is not: NULL covers both\n\"placed, matched nothing\" and \"nothing was derived\", so it needs the\nbackfill to have run AND the org to have active coordinate-based coverage\n(an org with name-based areas only resolves nothing, and every project would\nread as outside). See the predicate comment in buildProjectFilters.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "travelFeeZoneIds",
            "required": false,
            "in": "query",
            "description": "Travel-fee zone, matched against the DERIVED\n`Project.resolvedTravelFeeZoneId`. Zones are the concentric distance bands\nconfigured inside each service area (they live only in the\n`Organization.serviceArea` JSON — there is no zone table), so a zone id is\nonly meaningful next to its area; pair this with `serviceAreaIds`.\n\nWithheld until now for the same reason as serviceAreaIds: the\n`Project.travelFee*` columns are the immutable billing snapshot, frozen so\nthat moving a zone boundary can never rewrite a historical invoice, and 0%\npopulated. \"Which zone is this property in today\" is a different question,\nand it gets its own derived column.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "editorId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pipelineStageId",
            "required": false,
            "in": "query",
            "description": "Kanban stage membership filter (see ProjectFilterParams.pipelineStageId).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "projectManagerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianAssigned",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "importSource",
            "required": false,
            "in": "query",
            "description": "Filter by import provenance (Project.importSource) — e.g. 'aryeo' to select\nonly Aryeo-imported projects. Lets \"select all across pages\" resolve the\nAryeo-only subset server-side (the loaded page can't be filtered for the\noff-page rows client-side, since importSource only rides on loaded cards).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "schedulingStatus",
            "required": false,
            "in": "query",
            "description": "Partition the list by scheduling state:\n  - 'pending'   → only projects with no shoot time yet (scheduledTime null)\n  - 'scheduled' → only projects that have a shoot time (scheduledTime set)\nOmitted → both are returned (legacy behaviour). The company Projects\npage splits into a \"scheduling pending\" carousel ('pending') and the\nmain paginated grid ('scheduled') so the grid's page count reflects\nscheduled projects only.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "view",
            "required": false,
            "in": "query",
            "description": "Response shape. `card` returns a LEAN per-project payload with only the\nrelations a list/board card renders (dropping the deep joins the detail\nview uses), which massively cuts the query + JSON for card grids like the\nkanban board. Omitted/`full` keeps the historic fat include. Consumers that\npass `card` must hydrate full detail separately (e.g. on opening a job).",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "packageId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mediaTypes",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "addOnIds",
            "required": false,
            "in": "query",
            "description": "Add-ons selected at booking (`Project.selectedAddOnIds`). Any-of: a\nproject matches when it carries at least one of the selected add-ons.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "includeArchived",
            "required": false,
            "in": "query",
            "description": "When true, archived projects are included in the result. Defaults to\nfalse (archived projects are hidden). Only the deep \"Archived Orders\"\nsettings page should pass true.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "archivedOnly",
            "required": false,
            "in": "query",
            "description": "When true, returns ONLY archived orders — the projects-page \"Archived\"\nfilter. Distinct from includeArchived (which returns both). Also opts out\nof the default cancelled-exclusion since archived orders are cancelled.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "workspaceOrgId",
            "required": false,
            "in": "query",
            "description": "For AGENT users only: when set, scopes the cross-org \"my projects\"\nlist to a specific workspace.\n- If the org is COMPANY/TEAM: returns only projects fulfilled by that org.\n- If the org is the agent's PERSONAL org: returns only projects fulfilled\n  by solo providers (PERSONAL orgs).\n- Otherwise: ignored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get project metrics",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/nearest-scheduled": {
      "get": {
        "operationId": "ProjectsController_getNearestScheduled",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get nearest scheduled project date",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/delivery-templates": {
      "get": {
        "operationId": "ProjectsController_listDeliveryTemplates",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List delivery templates for org",
        "tags": [
          "Projects"
        ]
      },
      "post": {
        "operationId": "ProjectsController_createDeliveryTemplate",
        "parameters": [],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create a delivery template",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/delivery-templates/{templateId}": {
      "patch": {
        "operationId": "ProjectsController_updateDeliveryTemplate",
        "parameters": [
          {
            "name": "templateId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update a delivery template",
        "tags": [
          "Projects"
        ]
      },
      "delete": {
        "operationId": "ProjectsController_deleteDeliveryTemplate",
        "parameters": [
          {
            "name": "templateId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Delete a delivery template",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/scan-results": {
      "post": {
        "operationId": "ProjectsController_attachScanResults",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Attach Vremly Scan results (tour URL, floorplan) to project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{projectId}/media": {
      "get": {
        "operationId": "ProjectsController_getProjectMedia",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List media for a project",
        "tags": [
          "Projects"
        ]
      },
      "post": {
        "operationId": "ProjectsController_addProjectMedia",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateProjectMediaDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Add media to a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{projectId}/media/importable": {
      "get": {
        "operationId": "ProjectsController_listImportableMedia",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sourceProjectId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List media on another job that can be imported",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{projectId}/media/import": {
      "post": {
        "operationId": "ProjectsController_importProjectMedia",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportProjectMediaDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Import media from another job",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{projectId}/drive-import": {
      "post": {
        "operationId": "ProjectsController_importDriveFiles",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportDriveFilesDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Import files from the order's Drive folder",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{projectId}/media/{mediaId}": {
      "delete": {
        "operationId": "ProjectsController_deleteProjectMedia",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mediaId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Delete media from a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/mine": {
      "get": {
        "operationId": "ProjectsController_findMine",
        "parameters": [
          {
            "name": "scheduledAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scheduledBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "deliveredAfter",
            "required": false,
            "in": "query",
            "description": "Delivery-date range, filtering `Project.deliveryEnabledAt`.\n\nIMPORTANT: `deliveryEnabledAt` is the LATEST delivery — it is overwritten\nevery time the project is delivered again (revision re-deliveries,\nre-sends), so this filter means \"most recently delivered in this range\",\nnot \"was ever delivered in this range\". A project delivered in March and\nre-delivered in May does NOT match a March range.\n\nA bare `YYYY-MM-DD` covers the whole UTC day on both ends, so\nafter = before = one day returns that day's deliveries.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "deliveredBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "firstDeliveredAfter",
            "required": false,
            "in": "query",
            "description": "FIRST-delivery range, filtering `Project.firstDeliveredAt` — written once,\nthe first time the job was delivered, and never overwritten.\n\nThis is the counterpart to deliveredAfter/deliveredBefore above, which read\n`deliveryEnabledAt` (the LATEST delivery). Both exist because both questions\nare real and they give different answers whenever a job is re-delivered:\n\"what did we deliver in March\" wants the first delivery, \"what is live /\nmost recently delivered\" wants the latest. A job first delivered in March\nand re-delivered in July is in a March range here, and in a July range\nthere.\n\nSame bare-date semantics as every other range: a `YYYY-MM-DD` covers the\nwhole UTC day on both ends, so after = before = one day returns that day.\n\nThe column is NULL for rows delivered before it existed, so this range only\nsees deliveries actually on record — it never infers one from\n`deliveryEnabledAt`, which would re-import exactly the ambiguity it exists\nto remove.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "firstDeliveredBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Object"
              }
            }
          },
          {
            "name": "paymentStatus",
            "required": false,
            "in": "query",
            "description": "Payment status filter: 'paid' → paidAt set; 'unpaid' → paidAt null.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "customerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianId",
            "required": false,
            "in": "query",
            "description": "Single technician (saved views + deep links keep sending this). Merged\nserver-side with `technicianIds`, deduped — the two are one filter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianIds",
            "required": false,
            "in": "query",
            "description": "Multi-select technicians. Matches a person assigned ANYWHERE on the\nproject: the legacy `Project.technicianId` scalar, the ProjectTechnician\njoin, a shoot's own technician, or a shoot's crew.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "technicianMatch",
            "required": false,
            "in": "query",
            "description": "How to combine `technicianIds`:\n  - 'any' (default) → the project has at least one of them assigned\n  - 'all'           → EVERY selected person is assigned somewhere on the\n                      project. Deliberately not \"all on the same shoot\" —\n                      crews are per-visit, and the useful question when\n                      auditing a job is who worked it at all.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "crewSize",
            "required": false,
            "in": "query",
            "description": "Shoot-crew size — how many DISTINCT technicians are staffed on the job,\ncounted across all four places one can be attached (the project scalar,\nProjectTechnician, a LIVE shoot's own technician, and that shoot's crew).\n\n  'solo' | '1'              → exactly one technician\n  'crew' | 'team' | 'multi' → two or more\n  'N' | 'N+' (N = 1..50)    → N or more\n  '' (a cleared select)     → no filter\n\nUnassigned jobs (0 technicians) match NO crew size — that question is\n`technicianAssigned=false`.\n\nThe pattern deliberately mirrors ProjectsService.parseCrewSizeFilter, which\nowns this vocabulary: `whitelist: true` strips anything this DTO does not\ndeclare, so a value the parser understands but this regex rejects would be\nunreachable code, while anything this lets through that the parser does not\naccept (N outside 1..50) gets a 400 from the parser itself. Keep the two in\nstep.\n\nNOT applied by buildProjectFilters. Prisma cannot express a to-many\ncardinality in `where` (no `_count`, no self-correlation), so this resolves\nthrough a pre-query that is AND-ed onto the clause — and it has to happen on\nthe list, the ids (\"select all\") AND the metrics query, or the chip count\ndisagrees with the rows beneath it. ProjectsService.applyCrewSizeFilter is\nthe one place that does it, and every project-list surface routes through\nit.",
            "schema": {
              "pattern": "^(solo|crew|team|multi|\\d{1,2}\\+?)?$",
              "type": "string"
            }
          },
          {
            "name": "serviceAreaIds",
            "required": false,
            "in": "query",
            "description": "Service-area coverage, matched against the DERIVED\n`Project.resolvedServiceAreaId` (where the property falls TODAY, re-derived\nfrom lat/lng against the org's current areas) plus two sentinels for the\nstates that have no id:\n  - '__outside__' → placed, and inside no area\n  - '__unknown__' → could not be placed at all (no usable coordinates)\nThey are not uuids, so they cannot collide with a real area id. See\nSERVICE_AREA_OUTSIDE / SERVICE_AREA_UNKNOWN in common/utils/filter-builder.\n\nThis filter existed as a correct predicate for a while but was withheld\nfrom the DTO, because it pointed at the frozen `Project.serviceAreaId` —\na booking-time BILLING snapshot that InvoicesService reads to choose\nper-area pricing, and that carries a value on ~no rows (0 of 3218 on dev).\nFiltering it matched nothing, and backfilling it would have changed what\nun-invoiced jobs cost. The derived column exists so filtering can be right\nwithout money moving; the frozen column stays frozen.\n\n'__unknown__' is always trustworthy. '__outside__' is not: NULL covers both\n\"placed, matched nothing\" and \"nothing was derived\", so it needs the\nbackfill to have run AND the org to have active coordinate-based coverage\n(an org with name-based areas only resolves nothing, and every project would\nread as outside). See the predicate comment in buildProjectFilters.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "travelFeeZoneIds",
            "required": false,
            "in": "query",
            "description": "Travel-fee zone, matched against the DERIVED\n`Project.resolvedTravelFeeZoneId`. Zones are the concentric distance bands\nconfigured inside each service area (they live only in the\n`Organization.serviceArea` JSON — there is no zone table), so a zone id is\nonly meaningful next to its area; pair this with `serviceAreaIds`.\n\nWithheld until now for the same reason as serviceAreaIds: the\n`Project.travelFee*` columns are the immutable billing snapshot, frozen so\nthat moving a zone boundary can never rewrite a historical invoice, and 0%\npopulated. \"Which zone is this property in today\" is a different question,\nand it gets its own derived column.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "editorId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pipelineStageId",
            "required": false,
            "in": "query",
            "description": "Kanban stage membership filter (see ProjectFilterParams.pipelineStageId).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "projectManagerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianAssigned",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "importSource",
            "required": false,
            "in": "query",
            "description": "Filter by import provenance (Project.importSource) — e.g. 'aryeo' to select\nonly Aryeo-imported projects. Lets \"select all across pages\" resolve the\nAryeo-only subset server-side (the loaded page can't be filtered for the\noff-page rows client-side, since importSource only rides on loaded cards).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "schedulingStatus",
            "required": false,
            "in": "query",
            "description": "Partition the list by scheduling state:\n  - 'pending'   → only projects with no shoot time yet (scheduledTime null)\n  - 'scheduled' → only projects that have a shoot time (scheduledTime set)\nOmitted → both are returned (legacy behaviour). The company Projects\npage splits into a \"scheduling pending\" carousel ('pending') and the\nmain paginated grid ('scheduled') so the grid's page count reflects\nscheduled projects only.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "view",
            "required": false,
            "in": "query",
            "description": "Response shape. `card` returns a LEAN per-project payload with only the\nrelations a list/board card renders (dropping the deep joins the detail\nview uses), which massively cuts the query + JSON for card grids like the\nkanban board. Omitted/`full` keeps the historic fat include. Consumers that\npass `card` must hydrate full detail separately (e.g. on opening a job).",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "packageId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mediaTypes",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "addOnIds",
            "required": false,
            "in": "query",
            "description": "Add-ons selected at booking (`Project.selectedAddOnIds`). Any-of: a\nproject matches when it carries at least one of the selected add-ons.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "includeArchived",
            "required": false,
            "in": "query",
            "description": "When true, archived projects are included in the result. Defaults to\nfalse (archived projects are hidden). Only the deep \"Archived Orders\"\nsettings page should pass true.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "archivedOnly",
            "required": false,
            "in": "query",
            "description": "When true, returns ONLY archived orders — the projects-page \"Archived\"\nfilter. Distinct from includeArchived (which returns both). Also opts out\nof the default cancelled-exclusion since archived orders are cancelled.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "workspaceOrgId",
            "required": false,
            "in": "query",
            "description": "For AGENT users only: when set, scopes the cross-org \"my projects\"\nlist to a specific workspace.\n- If the org is COMPANY/TEAM: returns only projects fulfilled by that org.\n- If the org is the agent's PERSONAL org: returns only projects fulfilled\n  by solo providers (PERSONAL orgs).\n- Otherwise: ignored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List current user projects",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/messages": {
      "get": {
        "operationId": "ProjectsController_getMessages",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "channel",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get messages for a project",
        "tags": [
          "Projects"
        ]
      },
      "post": {
        "operationId": "ProjectsController_addMessage",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateMessageDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Add a message to a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/activity": {
      "get": {
        "operationId": "ProjectsController_getActivity",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get activity (audit) history for a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/create": {
      "post": {
        "operationId": "ProjectsController_createProject",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateProjectDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create a new project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}": {
      "get": {
        "operationId": "ProjectsController_findOne",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get project by ID",
        "tags": [
          "Projects"
        ]
      },
      "patch": {
        "operationId": "ProjectsController_updateProject",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateProjectDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update a project",
        "tags": [
          "Projects"
        ]
      },
      "delete": {
        "operationId": "ProjectsController_remove",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Permanently delete an archived project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/geocode": {
      "post": {
        "operationId": "ProjectsController_geocodeProject",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Re-geocode this project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/assign": {
      "patch": {
        "operationId": "ProjectsController_assign",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AssignProjectDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Assign technician and editor to a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/listing-status": {
      "patch": {
        "operationId": "ProjectsController_updateListingStatus",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateListingStatusDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update MLS listing status (live / sold)",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/notes": {
      "patch": {
        "operationId": "ProjectsController_updateNotes",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update a project’s notes",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/cancel": {
      "post": {
        "operationId": "ProjectsController_cancel",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CancelProjectDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Cancel a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/archive": {
      "post": {
        "operationId": "ProjectsController_archive",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Archive a cancelled project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/unarchive": {
      "post": {
        "operationId": "ProjectsController_unarchive",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Unarchive a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/bulk-delete": {
      "post": {
        "operationId": "ProjectsController_bulkRemove",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkDeleteProjectsDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Bulk delete projects",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/assign-technician": {
      "patch": {
        "operationId": "ProjectsController_assignTechnician",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Assign technician to a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/technician-distances": {
      "get": {
        "operationId": "ProjectsController_technicianDistances",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Route-aware technician distances for a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/assign-customer": {
      "patch": {
        "operationId": "ProjectsController_assignCustomer",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Assign customer to a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/assign-project-manager": {
      "patch": {
        "operationId": "ProjectsController_assignProjectManager",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Assign project manager",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/agent-past-technicians": {
      "get": {
        "operationId": "ProjectsController_getAgentPastTechnicians",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get technicians from agent past jobs for this project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/agent-request-technician": {
      "patch": {
        "operationId": "ProjectsController_agentRequestTechnician",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Agent requests a different technician from past jobs",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/technicians/{technicianId}": {
      "delete": {
        "operationId": "ProjectsController_removeTechnician",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Remove technician from a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/editors/{editorId}": {
      "delete": {
        "operationId": "ProjectsController_removeEditor",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "editorId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Remove editor from a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/customers/{customerId}": {
      "delete": {
        "operationId": "ProjectsController_removeCustomer",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "customerId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Remove customer from a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/assign-editor": {
      "patch": {
        "operationId": "ProjectsController_assignEditor",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Assign editor to a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/bulk/editors": {
      "patch": {
        "operationId": "ProjectsController_bulkSetEditors",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkSetEditorsDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Bulk add/remove editors across projects",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/assignment-batches/{batchId}/flush": {
      "post": {
        "operationId": "ProjectsController_flushAssignmentBatch",
        "parameters": [
          {
            "name": "batchId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Flush a bulk-assignment notification batch",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/schedule": {
      "patch": {
        "operationId": "ProjectsController_scheduleProject",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Schedule a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/status": {
      "patch": {
        "operationId": "ProjectsController_updateStatus",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateProjectStatusDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update project status",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/stage": {
      "patch": {
        "operationId": "ProjectsController_moveToStage",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/start-editing": {
      "post": {
        "operationId": "ProjectsController_startEditing",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Confirm editing work has begun on a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/technician-workflow": {
      "patch": {
        "operationId": "ProjectsController_setTechnicianWorkflow",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Record technician workflow acknowledgements",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/handoff": {
      "post": {
        "operationId": "ProjectsController_handoff",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Technician hand-off — raw files are in",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/showcase": {
      "patch": {
        "operationId": "ProjectsController_toggleShowcase",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Toggle showcase flag on a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/paywall-bypass": {
      "patch": {
        "operationId": "ProjectsController_setPaywallBypass",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Unlock or re-lock the delivery paywall for a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/delivery": {
      "get": {
        "operationId": "ProjectsController_getDeliveryStatus",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get delivery status for a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/delivery/email-preview": {
      "get": {
        "operationId": "ProjectsController_previewDeliveryEmail",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "message",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mediaTypes",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "clientDeliveryNote",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Render the exact delivery email for a project (including the project-details block) without sending it",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/delivery/enable": {
      "post": {
        "operationId": "ProjectsController_enableDelivery",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EnableDeliveryDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Enable delivery for a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/delivery/disable": {
      "post": {
        "operationId": "ProjectsController_disableDelivery",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Disable delivery for a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/delivery/regenerate-token": {
      "post": {
        "operationId": "ProjectsController_regenerateDeliveryToken",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Regenerate delivery token",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/delivery/rotate-token": {
      "post": {
        "operationId": "ProjectsController_rotateDeliveryToken",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Rotate delivery token",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/visits": {
      "get": {
        "operationId": "ProjectsController_getVisits",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List visits for a project",
        "tags": [
          "Projects"
        ]
      },
      "post": {
        "operationId": "ProjectsController_createVisit",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateVisitDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create a visit for a project",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/shoots": {
      "post": {
        "operationId": "ProjectsController_addShoot",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddShootDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Add a shoot (visit) to an order with a pricing intent (multi-appointments)",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/visits/{visitId}": {
      "patch": {
        "operationId": "ProjectsController_updateVisit",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "visitId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateVisitDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update a visit",
        "tags": [
          "Projects"
        ]
      },
      "delete": {
        "operationId": "ProjectsController_cancelVisit",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "visitId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "permanent",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Cancel a visit (soft) or permanently delete it (?permanent=true)",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/visits/{visitId}/approval": {
      "patch": {
        "operationId": "ProjectsController_approveVisit",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "visitId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApproveVisitDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Approve or reject a visit",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/visits/{visitId}/start-locations": {
      "get": {
        "operationId": "ProjectsController_getVisitStartLocations",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "visitId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Per-crew-member start locations for one shoot",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/{id}/visits/{visitId}/start-locations/{userId}": {
      "put": {
        "operationId": "ProjectsController_setVisitStartLocation",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "visitId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetVisitStartLocationDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Set one crew member's start location for this shoot",
        "tags": [
          "Projects"
        ]
      },
      "delete": {
        "operationId": "ProjectsController_clearVisitStartLocation",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "visitId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Clear a start location (falls back to home base)",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/backfill-service-areas": {
      "post": {
        "operationId": "ProjectsController_backfillServiceAreas",
        "parameters": [],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Backfill service areas for projects missing one",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/backfill-initial-visits": {
      "post": {
        "operationId": "ProjectsController_backfillInitialVisits",
        "parameters": [],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Backfill the initial visit for scheduled orders missing one",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/backfill-derived-locations": {
      "post": {
        "operationId": "ProjectsController_backfillDerivedLocations",
        "parameters": [
          {
            "name": "apply",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Backfill the derived service-area / travel-fee-zone filter columns (dry-run unless apply=true)",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/backfill-first-delivered": {
      "post": {
        "operationId": "ProjectsController_backfillFirstDeliveredAt",
        "parameters": [
          {
            "name": "apply",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Backfill Project.firstDeliveredAt from delivery history (dry-run unless apply=true)",
        "tags": [
          "Projects"
        ]
      }
    },
    "/projects/fix-coordinates": {
      "post": {
        "operationId": "ProjectsController_fixBadCoordinates",
        "parameters": [],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Fix projects with bad coordinates by re-geocoding",
        "tags": [
          "Projects"
        ]
      }
    },
    "/availability": {
      "get": {
        "operationId": "InternalAvailabilityController_getAvailability",
        "parameters": [
          {
            "name": "startDate",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "endDate",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianIds",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "duration",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "targetOrgId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get technician availability slots",
        "tags": [
          "Availability"
        ]
      }
    },
    "/availability/check": {
      "get": {
        "operationId": "InternalAvailabilityController_checkSlot",
        "parameters": [
          {
            "name": "technicianId",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "scheduledTime",
            "required": true,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "duration",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Check if a specific time slot is available",
        "tags": [
          "Availability"
        ]
      }
    },
    "/media/presign": {
      "post": {
        "operationId": "MediaController_presignUpload",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PresignUploadDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get a presigned S3 upload URL for project media",
        "tags": [
          "Media"
        ]
      }
    },
    "/media/confirm-upload": {
      "post": {
        "operationId": "MediaController_confirmUpload",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConfirmUploadDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Confirm a media upload",
        "tags": [
          "Media"
        ]
      }
    },
    "/media/project/{projectId}": {
      "get": {
        "operationId": "MediaController_getMediaForProject",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get media for a project",
        "tags": [
          "Media"
        ]
      }
    },
    "/media/{id}": {
      "get": {
        "operationId": "MediaController_getMediaById",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get media by ID",
        "tags": [
          "Media"
        ]
      },
      "delete": {
        "operationId": "MediaController_deleteMedia",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Delete media by ID",
        "tags": [
          "Media"
        ]
      }
    },
    "/media/reorder": {
      "patch": {
        "operationId": "MediaController_reorderMedia",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Reorder media items within a project",
        "tags": [
          "Media"
        ]
      }
    },
    "/media/{id}/rename": {
      "patch": {
        "operationId": "MediaController_renameMedia",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Rename a media item",
        "tags": [
          "Media"
        ]
      }
    },
    "/user-availability": {
      "get": {
        "operationId": "AvailabilityController_getMyAvailability",
        "parameters": [],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get current user availability settings",
        "tags": [
          "Availability"
        ]
      }
    },
    "/user-availability/status": {
      "patch": {
        "operationId": "AvailabilityController_updateAvailabilityStatus",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AvailabilityStatusDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update availability status",
        "tags": [
          "Availability"
        ]
      }
    },
    "/user-availability/work-hours": {
      "patch": {
        "operationId": "AvailabilityController_updateWorkHours",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WorkHoursDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update work hours for a specific day",
        "tags": [
          "Availability"
        ]
      },
      "put": {
        "operationId": "AvailabilityController_updateAllWorkHours",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update all work hours at once",
        "tags": [
          "Availability"
        ]
      }
    },
    "/webhooks/subscriptions": {
      "post": {
        "operationId": "WebhooksController_create",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookSubscriptionDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create a webhook subscription",
        "tags": [
          "Webhooks"
        ]
      },
      "get": {
        "operationId": "WebhooksController_list",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List webhook subscriptions",
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/webhooks/subscriptions/{id}": {
      "patch": {
        "operationId": "WebhooksController_update",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateWebhookSubscriptionDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update a webhook subscription",
        "tags": [
          "Webhooks"
        ]
      },
      "delete": {
        "operationId": "WebhooksController_delete",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Delete a webhook subscription",
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/webhooks/subscriptions/{id}/deliveries": {
      "get": {
        "operationId": "WebhooksController_listDeliveries",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List delivery log for a subscription",
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/webhooks/subscriptions/{id}/test": {
      "post": {
        "operationId": "WebhooksController_testPing",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Send a test ping to a subscription",
        "tags": [
          "Webhooks"
        ]
      }
    },
    "/invoices": {
      "get": {
        "operationId": "InvoicesController_listInvoices",
        "parameters": [
          {
            "name": "status",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Object"
              }
            }
          },
          {
            "name": "customerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "projectId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List invoices for organization",
        "tags": [
          "Invoices"
        ]
      },
      "post": {
        "operationId": "InvoicesController_createInvoice",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateInvoiceDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create a new invoice",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/bulk": {
      "post": {
        "operationId": "InvoicesController_bulkAction",
        "parameters": [],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Run a bulk operation on a selection of invoices",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/reports/revenue": {
      "get": {
        "operationId": "InvoicesController_getRevenueReport",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Revenue summary for the Reports page",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/reports/pipeline": {
      "get": {
        "operationId": "InvoicesController_getPipelineReport",
        "parameters": [],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Committed-but-not-collected pipeline for the Reports page",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/reports/stripe-reconciliation": {
      "get": {
        "operationId": "InvoicesController_probeStripeReconciliation",
        "parameters": [],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Read-only Stripe reconciliation probe for imported invoices",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/reports/tax": {
      "get": {
        "operationId": "InvoicesController_getTaxReport",
        "parameters": [
          {
            "name": "period",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Tax collected aggregated by type and period",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/metrics": {
      "get": {
        "operationId": "InvoicesController_getInvoiceMetrics",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get invoice counts + totals grouped by status",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/my": {
      "get": {
        "operationId": "InvoicesController_listMyInvoices",
        "parameters": [],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List invoices issued to the current user as a customer",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/pay-sheet": {
      "post": {
        "operationId": "InvoicesController_createInvoicePaymentSheet",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayInvoiceDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create native PaymentSheet bundle to pay an invoice",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/checkout-session": {
      "post": {
        "operationId": "InvoicesController_createAuthenticatedPaymentSession",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PayInvoiceDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create a hosted Checkout session to pay an invoice (authenticated)",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/customer-balance": {
      "get": {
        "operationId": "InvoicesController_getCustomerBalance",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Get customer outstanding balance",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/by-project/{projectId}": {
      "get": {
        "operationId": "InvoicesController_getInvoiceByProject",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get invoice by project ID",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}": {
      "get": {
        "operationId": "InvoicesController_getInvoice",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get invoice by ID",
        "tags": [
          "Invoices"
        ]
      },
      "patch": {
        "operationId": "InvoicesController_updateInvoice",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateInvoiceDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update an invoice",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/view": {
      "get": {
        "operationId": "InvoicesController_getInvoiceForViewer",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "summary": "Invoice detail for a PAYER — the mobile invoice screen and the\nauthenticated web pay page both read this. Accessible to the invoice's\nbilled CUSTOMER, to an OWNER/ADMIN of the issuing org, and to a party\nattached to the invoice's project (active ProjectShare / co-customer).\nNo OrgRolesGuard — customers and shared agents aren't org members.\n\nA project party's payload is REDACTED: no `paymentToken`, and the billed\ncustomer is reduced to `{ id, name }`. See\n`redactInvoiceForProjectParty` — the response carries `viewerStanding` so\nthe client can tell \"redacted\" from \"no pay link exists\".",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/payment": {
      "get": {
        "operationId": "InvoicesController_getInvoicePayment",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Fetch Stripe-hydrated payment details for an invoice",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/manual-refunds": {
      "post": {
        "operationId": "InvoicesController_recordManualRefund",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Record a non-Stripe refund for audit",
        "tags": [
          "Invoices"
        ]
      },
      "get": {
        "operationId": "InvoicesController_listManualRefunds",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List manual refunds recorded against an invoice.\nGET /invoices/:id/manual-refunds",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/refunds": {
      "post": {
        "operationId": "InvoicesController_refundInvoice",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefundInvoiceDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Refund an invoice through Stripe",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/pdf": {
      "get": {
        "operationId": "InvoicesController_downloadPdf[0]",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "download",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "View or download invoice as PDF (inline by default)",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/pdf/{filename}": {
      "get": {
        "operationId": "InvoicesController_downloadPdf[1]",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "download",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "View or download invoice as PDF (inline by default)",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/public/{token}": {
      "get": {
        "operationId": "InvoicesController_getInvoiceByToken",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Get invoice by payment token (public)",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/public/{token}/pdf": {
      "get": {
        "operationId": "InvoicesController_downloadPublicPdf[0]",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "download",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "View or download invoice PDF by public token",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/public/{token}/pdf/{filename}": {
      "get": {
        "operationId": "InvoicesController_downloadPublicPdf[1]",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "download",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "View or download invoice PDF by public token",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/public/{token}/pay": {
      "post": {
        "operationId": "InvoicesController_createPaymentSession",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          }
        },
        "summary": "Create payment session for invoice (public)",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/from-project/{projectId}": {
      "post": {
        "operationId": "InvoicesController_createFromProject",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "forceNew",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create invoice from project package",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/reconcile": {
      "post": {
        "operationId": "InvoicesController_reconcileInvoice",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Reconcile invoice refund state from Stripe",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/resync/preview": {
      "post": {
        "operationId": "InvoicesController_previewInvoiceResync",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Preview rebuilding a draft from its project",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/resync": {
      "post": {
        "operationId": "InvoicesController_resyncInvoiceFromProject",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResyncInvoiceDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Rebuild a draft invoice from its project",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/by-project/{projectId}/drift": {
      "get": {
        "operationId": "InvoicesController_getProjectBillingDrift",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Differences between a job's invoices and the job as it stands",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/scope-change/confirm": {
      "post": {
        "operationId": "InvoicesController_confirmInvoiceScopeChange",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConfirmScopeChangeDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update a sent invoice after a package change",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/scope-change/supplementary": {
      "post": {
        "operationId": "InvoicesController_createSupplementaryScopeInvoice",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSupplementaryInvoiceDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Raise a supplementary invoice for a post-payment package change",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/send": {
      "post": {
        "operationId": "InvoicesController_sendInvoice",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Send invoice to customer",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/send-payment-link": {
      "post": {
        "operationId": "InvoicesController_sendPaymentLink",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Send the customer a payment link (email + SMS)",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/void": {
      "post": {
        "operationId": "InvoicesController_voidInvoice",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Void an invoice",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/mark-paid": {
      "post": {
        "operationId": "InvoicesController_markInvoicePaid",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MarkInvoicePaidDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Mark an invoice paid (off-platform payment)",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/payment-methods": {
      "get": {
        "operationId": "InvoicesController_getCustomerPaymentMethods",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get customer payment methods for invoice",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/charge": {
      "post": {
        "operationId": "InvoicesController_chargeInvoice",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Charge customer card on file",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/delta": {
      "get": {
        "operationId": "InvoicesController_getInvoiceDelta",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Delta between an invoice and what was collected",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/execute-adjustment-refund": {
      "post": {
        "operationId": "InvoicesController_executeAdjustmentRefund",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Return the overpayment on a credit note",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/record-adjustment-return": {
      "post": {
        "operationId": "InvoicesController_recordAdjustmentReturn",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Record a manual return against a credit note",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/apply-discount": {
      "post": {
        "operationId": "InvoicesController_applyDiscount",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApplyDiscountDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Apply a coupon / discount to an invoice",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/invoices/{id}/remove-discount": {
      "post": {
        "operationId": "InvoicesController_removeDiscount",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Remove the applied coupon / discount from an invoice",
        "tags": [
          "Invoices"
        ]
      }
    },
    "/listings/projects/{projectId}/video-scripts": {
      "get": {
        "operationId": "VideoScriptsController_get",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "This listing's AI video scripts",
        "tags": [
          "listings"
        ]
      }
    },
    "/listings/projects/{projectId}/video-scripts/feedback": {
      "post": {
        "operationId": "VideoScriptsController_submitFeedback",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitVideoScriptFeedbackDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Leave feedback on this listing's video scripts",
        "tags": [
          "listings"
        ]
      }
    },
    "/listings/projects/{projectId}/video-scripts/generate": {
      "post": {
        "operationId": "VideoScriptsController_generate",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GenerateVideoScriptsDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Generate AI video scripts for this listing",
        "tags": [
          "listings"
        ]
      }
    },
    "/listings/projects/{projectId}/video-scripts/variations/{variationId}/select": {
      "patch": {
        "operationId": "VideoScriptsController_select",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "variationId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Pick the script this listing will use",
        "tags": [
          "listings"
        ]
      }
    },
    "/packages/org/{orgId}": {
      "get": {
        "operationId": "PackagesController_getPackagesForOrg",
        "parameters": [
          {
            "name": "orgId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lat",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lng",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Get active packages for an organization",
        "tags": [
          "Packages"
        ]
      }
    },
    "/packages/org/{orgId}/addons": {
      "get": {
        "operationId": "PackagesController_getAddOnsForOrg",
        "parameters": [
          {
            "name": "orgId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lat",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lng",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Get active add-ons for an organization",
        "tags": [
          "Packages"
        ]
      }
    },
    "/packages/calculate": {
      "post": {
        "operationId": "PackagesController_calculateTotal",
        "parameters": [],
        "responses": {
          "201": {
            "description": ""
          }
        },
        "summary": "Quote a package + add-ons cart (subtotal and tax)",
        "tags": [
          "Packages"
        ]
      }
    },
    "/packages/calculate-custom": {
      "post": {
        "operationId": "PackagesController_calculateCustomTotal",
        "parameters": [],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Quote a Build-Your-Own custom cart (subtotal and tax)",
        "tags": [
          "Packages"
        ]
      }
    },
    "/packages/public/org/{orgId}/calculate-custom": {
      "post": {
        "operationId": "PackagesController_calculatePublicCustomTotal",
        "parameters": [
          {
            "name": "orgId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          }
        },
        "summary": "Public Build-Your-Own custom cart quote",
        "tags": [
          "Packages"
        ]
      }
    },
    "/packages": {
      "get": {
        "operationId": "PackagesController_getAllPackages",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List all packages including inactive",
        "tags": [
          "Packages"
        ]
      },
      "post": {
        "operationId": "PackagesController_createPackage",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePackageDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create a package",
        "tags": [
          "Packages"
        ]
      }
    },
    "/packages/addons": {
      "get": {
        "operationId": "PackagesController_getAllAddOns",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List all add-ons including inactive",
        "tags": [
          "Packages"
        ]
      },
      "post": {
        "operationId": "PackagesController_createAddOn",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAddOnDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create an add-on",
        "tags": [
          "Packages"
        ]
      }
    },
    "/packages/{id}/duplicate": {
      "post": {
        "operationId": "PackagesController_duplicatePackage",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Duplicate a package",
        "tags": [
          "Packages"
        ]
      }
    },
    "/packages/{id}": {
      "put": {
        "operationId": "PackagesController_updatePackage",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdatePackageDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update a package",
        "tags": [
          "Packages"
        ]
      },
      "delete": {
        "operationId": "PackagesController_deletePackage",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Delete a package",
        "tags": [
          "Packages"
        ]
      }
    },
    "/packages/addons/{id}": {
      "put": {
        "operationId": "PackagesController_updateAddOn",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateAddOnDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update an add-on",
        "tags": [
          "Packages"
        ]
      },
      "delete": {
        "operationId": "PackagesController_deleteAddOn",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Delete an add-on",
        "tags": [
          "Packages"
        ]
      }
    },
    "/packages/addons/{id}/auto-hidden-packages": {
      "get": {
        "operationId": "PackagesController_getAutoHiddenPackagesForAddOn",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Auto-hide preview for an add-on",
        "tags": [
          "Packages"
        ]
      }
    },
    "/inquiries": {
      "post": {
        "operationId": "InquiriesController_createInquiry",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateInquiryDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          }
        },
        "summary": "Submit a public inquiry",
        "tags": [
          "Inquiries"
        ]
      },
      "get": {
        "operationId": "InquiriesController_getAllInquiries",
        "parameters": [
          {
            "name": "status",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List all inquiries (paginated)",
        "tags": [
          "Inquiries"
        ]
      }
    },
    "/inquiries/{id}": {
      "get": {
        "operationId": "InquiriesController_getInquiryById",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get inquiry by ID",
        "tags": [
          "Inquiries"
        ]
      },
      "patch": {
        "operationId": "InquiriesController_updateInquiry",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateInquiryDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update an inquiry",
        "tags": [
          "Inquiries"
        ]
      }
    },
    "/inquiries/{id}/convert": {
      "post": {
        "operationId": "InquiriesController_convertToProject",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Convert inquiry to a project",
        "tags": [
          "Inquiries"
        ]
      }
    },
    "/organizations": {
      "get": {
        "operationId": "OrganizationsController_listMyOrgs",
        "parameters": [],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List my organizations",
        "tags": [
          "Organizations"
        ]
      }
    },
    "/customers/metrics": {
      "get": {
        "operationId": "CustomersController_getMetrics",
        "parameters": [
          {
            "name": "search",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "description": "'lastSeenAt' sorts through the linked User (OrganizationCustomer.user) —\ncustomers with no account sort as NULL, which the service pins last in\nboth directions so \"never seen\" never masquerades as \"seen longest ago\".\n\n'loyalty' sorts by the customer's EFFECTIVE loyalty ranking, through\nOrganizationCustomer.loyaltyAccount -> loyaltyRankCents. It is accepted\nunconditionally here because @IsIn cannot know whether the CALLER's org has\nthe loyalty programme switched on — that gate lives in the service, which\nhas `ctx.org.loyaltyEnabled` and quietly falls back to the default ordering.\nA validator that 400'd on it would turn a stale bookmark into a broken page\nthe moment an org switched loyalty off.\n\nAdding a value to this list is safe for existing clients: under\n`forbidNonWhitelisted` a new FIELD would 400, but a new enum member cannot.",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "email",
                "notes",
                "createdAt",
                "totalJobs",
                "lastSeenAt",
                "loyalty"
              ]
            }
          },
          {
            "name": "onPlatform",
            "required": false,
            "in": "query",
            "description": "'true' = on platform (signed-up); 'pending' = invited but not yet joined\n(returning-customer set-up email sent, no account yet); 'false' = never\ninvited and no account; omit = all.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get customer metrics",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers": {
      "get": {
        "operationId": "CustomersController_list",
        "parameters": [
          {
            "name": "search",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "description": "'lastSeenAt' sorts through the linked User (OrganizationCustomer.user) —\ncustomers with no account sort as NULL, which the service pins last in\nboth directions so \"never seen\" never masquerades as \"seen longest ago\".\n\n'loyalty' sorts by the customer's EFFECTIVE loyalty ranking, through\nOrganizationCustomer.loyaltyAccount -> loyaltyRankCents. It is accepted\nunconditionally here because @IsIn cannot know whether the CALLER's org has\nthe loyalty programme switched on — that gate lives in the service, which\nhas `ctx.org.loyaltyEnabled` and quietly falls back to the default ordering.\nA validator that 400'd on it would turn a stale bookmark into a broken page\nthe moment an org switched loyalty off.\n\nAdding a value to this list is safe for existing clients: under\n`forbidNonWhitelisted` a new FIELD would 400, but a new enum member cannot.",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "email",
                "notes",
                "createdAt",
                "totalJobs",
                "lastSeenAt",
                "loyalty"
              ]
            }
          },
          {
            "name": "onPlatform",
            "required": false,
            "in": "query",
            "description": "'true' = on platform (signed-up); 'pending' = invited but not yet joined\n(returning-customer set-up email sent, no account yet); 'false' = never\ninvited and no account; omit = all.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List customers (with optional search)",
        "tags": [
          "Customers"
        ]
      },
      "post": {
        "operationId": "CustomersController_create",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCustomerDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create a customer",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/ids": {
      "get": {
        "operationId": "CustomersController_listIds",
        "parameters": [
          {
            "name": "search",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "description": "'lastSeenAt' sorts through the linked User (OrganizationCustomer.user) —\ncustomers with no account sort as NULL, which the service pins last in\nboth directions so \"never seen\" never masquerades as \"seen longest ago\".\n\n'loyalty' sorts by the customer's EFFECTIVE loyalty ranking, through\nOrganizationCustomer.loyaltyAccount -> loyaltyRankCents. It is accepted\nunconditionally here because @IsIn cannot know whether the CALLER's org has\nthe loyalty programme switched on — that gate lives in the service, which\nhas `ctx.org.loyaltyEnabled` and quietly falls back to the default ordering.\nA validator that 400'd on it would turn a stale bookmark into a broken page\nthe moment an org switched loyalty off.\n\nAdding a value to this list is safe for existing clients: under\n`forbidNonWhitelisted` a new FIELD would 400, but a new enum member cannot.",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "email",
                "notes",
                "createdAt",
                "totalJobs",
                "lastSeenAt",
                "loyalty"
              ]
            }
          },
          {
            "name": "onPlatform",
            "required": false,
            "in": "query",
            "description": "'true' = on platform (signed-up); 'pending' = invited but not yet joined\n(returning-customer set-up email sent, no account yet); 'false' = never\ninvited and no account; omit = all.",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get all customer IDs matching filters (for select-all)",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/returning/preview": {
      "post": {
        "operationId": "CustomersController_previewReturning",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MigrateReturningCustomersDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Preview eligible returning customers (migrated, not yet activated)",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/returning/migrate": {
      "post": {
        "operationId": "CustomersController_migrateReturning",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MigrateReturningCustomersDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Migrate eligible returning customers — create accounts + send set-up emails",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/{id}": {
      "get": {
        "operationId": "CustomersController_getDetail",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get a single customer with hydrated stats",
        "tags": [
          "Customers"
        ]
      },
      "patch": {
        "operationId": "CustomersController_update",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateCustomerDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update a customer",
        "tags": [
          "Customers"
        ]
      },
      "delete": {
        "operationId": "CustomersController_delete",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Delete a customer",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/{id}/sessions": {
      "get": {
        "operationId": "CustomersController_listSessions",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "A customer's active sessions and devices (IP redacted)",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/reports/top-by-spend": {
      "get": {
        "operationId": "CustomersController_topBySpend",
        "parameters": [
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Top customers by lifetime spend for Reports",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/reports/value-segmentation": {
      "get": {
        "operationId": "CustomersController_valueSegmentation",
        "parameters": [
          {
            "name": "window",
            "required": false,
            "in": "query",
            "description": "Time frame to segment over. Resolved in the organization timezone.",
            "schema": {
              "type": "string",
              "enum": [
                "this_month",
                "last_month",
                "this_quarter",
                "ytd",
                "last_12_months",
                "all_time"
              ]
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "description": "Rows per ranking (each ranking is sliced independently).",
            "schema": {
              "minimum": 1,
              "maximum": 100,
              "default": 20,
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Customer value segmentation over a time frame, for Reports",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/{id}/invite": {
      "post": {
        "operationId": "CustomersController_sendInvite",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Send platform invite to a customer",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/invite/bulk": {
      "post": {
        "operationId": "CustomersController_bulkInvite",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkInviteCustomersDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Bulk send platform invites to customers",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/{id}/send-password-reset": {
      "post": {
        "operationId": "CustomersController_sendPasswordReset",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Send a password reset email to a customer",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/{id}/payment-methods": {
      "get": {
        "operationId": "CustomersController_listPaymentMethods",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List saved payment methods for a customer",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/{id}/card-setup-intent": {
      "post": {
        "operationId": "CustomersController_createCardSetupIntent",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Start a card-capture session for a customer (staff-run)",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/{id}/send-card-setup-link": {
      "post": {
        "operationId": "CustomersController_sendCardSetupLink",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Email the customer a link to add a card on file",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/{id}/payment-methods/{pmId}": {
      "delete": {
        "operationId": "CustomersController_removePaymentMethod",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pmId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Remove a saved payment method from a customer",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/{id}/default-payment-method/{pmId}": {
      "patch": {
        "operationId": "CustomersController_setDefaultPaymentMethod",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pmId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Set a customer's default payment method",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/bulk-delete": {
      "post": {
        "operationId": "CustomersController_bulkDelete",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BulkDeleteCustomersDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Bulk delete customers",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/import": {
      "post": {
        "operationId": "CustomersController_import",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ImportCustomersDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Import customers + their agent teams from a CSV",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/team-invites/send": {
      "post": {
        "operationId": "CustomersController_sendTeamInvites",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendTeamInvitesDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Send team invitations created by a CSV import",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/team-invites/outstanding": {
      "post": {
        "operationId": "CustomersController_outstandingTeamInvites",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OutstandingTeamInvitesDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Team invitations still waiting to be sent",
        "tags": [
          "Customers"
        ]
      }
    },
    "/customers/portal-invites/send": {
      "post": {
        "operationId": "CustomersController_sendPortalInvites",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendPortalInvitesDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Send portal invitations flagged by a CSV import",
        "tags": [
          "Customers"
        ]
      }
    },
    "/orders/public": {
      "post": {
        "operationId": "OrdersController_createPublicOrder",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePublicOrderDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Submit a public booking order",
        "tags": [
          "Orders"
        ]
      }
    },
    "/orders/quote": {
      "post": {
        "operationId": "OrdersController_quoteCheckout",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckoutQuoteDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Quote a booking total including sales tax",
        "tags": [
          "Orders"
        ]
      }
    },
    "/orders/create": {
      "post": {
        "operationId": "OrdersController_createOrder",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateOrderDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create a new order",
        "tags": [
          "Orders"
        ]
      }
    },
    "/orders/checkout": {
      "post": {
        "operationId": "OrdersController_createCheckout",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateOrderDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create Stripe checkout session for an order",
        "tags": [
          "Orders"
        ]
      }
    },
    "/orders/status/{sessionId}": {
      "get": {
        "operationId": "OrdersController_getOrderStatus",
        "parameters": [
          {
            "name": "sessionId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get order status by Stripe session ID",
        "tags": [
          "Orders"
        ]
      }
    },
    "/orders": {
      "get": {
        "operationId": "OrdersController_listOrders",
        "parameters": [
          {
            "name": "status",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Object"
              }
            }
          },
          {
            "name": "customerId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "technicianId",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List all orders for organization",
        "tags": [
          "Orders"
        ]
      }
    },
    "/orders/{projectId}/cancel": {
      "delete": {
        "operationId": "OrdersController_cancelOrder",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "path",
            "required": false,
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/Object"
            }
          },
          {
            "name": "reason",
            "required": false,
            "in": "query",
            "schema": {
              "maxLength": 500,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Cancel an order as customer",
        "tags": [
          "Orders"
        ]
      }
    },
    "/orders/{projectId}/cancellation-policy": {
      "get": {
        "operationId": "OrdersController_getCancellationPolicy",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get cancellation fee policy for a project",
        "tags": [
          "Orders"
        ]
      }
    },
    "/delivery/{token}/comments": {
      "get": {
        "operationId": "DeliveryController_getComments",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CommentDto"
                  }
                }
              }
            }
          }
        },
        "summary": "Get comments for a delivery",
        "tags": [
          "Delivery"
        ]
      },
      "post": {
        "operationId": "DeliveryController_addComment",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddCommentDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommentDto"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Add a comment to a delivery",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/{token}/download-request": {
      "post": {
        "operationId": "DeliveryController_requestDownload",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DownloadAllDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          }
        },
        "summary": "Request a download artifact",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/{token}/download-status/{artifactId}": {
      "get": {
        "operationId": "DeliveryController_getDownloadStatus",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "artifactId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Get download artifact status",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/{token}/media/{mediaId}/access": {
      "get": {
        "operationId": "DeliveryController_resolveSignedMedia",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mediaId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "download",
            "required": false,
            "in": "query",
            "schema": {}
          },
          {
            "name": "variant",
            "required": false,
            "in": "query",
            "schema": {}
          },
          {
            "name": "signature",
            "required": false,
            "in": "query",
            "schema": {}
          },
          {
            "name": "expires",
            "required": false,
            "in": "query",
            "schema": {}
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Resolve signed delivery media access URL",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/{token}/artifacts/{artifactId}/access": {
      "get": {
        "operationId": "DeliveryController_resolveSignedArtifact",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "artifactId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "signature",
            "required": false,
            "in": "query",
            "schema": {}
          },
          {
            "name": "expires",
            "required": false,
            "in": "query",
            "schema": {}
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Resolve signed delivery artifact access URL",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/{token}/staff/download-urls": {
      "get": {
        "operationId": "DeliveryController_staffDownloadUrls",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Staff per-file download URLs for a delivery",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/{token}/staff/download-request": {
      "post": {
        "operationId": "DeliveryController_staffRequestDownload",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DownloadAllDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Staff bulk download request for a delivery",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/{token}/staff/download-status/{artifactId}": {
      "get": {
        "operationId": "DeliveryController_staffDownloadStatus",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "artifactId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Staff bulk download status for a delivery",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/{token}/approve": {
      "post": {
        "operationId": "DeliveryController_approveDelivery",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Approve a delivery",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/{token}/request-changes": {
      "post": {
        "operationId": "DeliveryController_requestChanges",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RequestChangesDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Request changes on a delivery",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/{token}/ratings": {
      "post": {
        "operationId": "DeliveryController_createRatings",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateProjectRatingsDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Submit project ratings",
        "tags": [
          "Delivery"
        ]
      },
      "get": {
        "operationId": "DeliveryController_getRatings",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Get project ratings",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/{token}/survey": {
      "get": {
        "operationId": "DeliveryController_getSurvey",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Get active survey for this delivery",
        "tags": [
          "Delivery"
        ]
      },
      "post": {
        "operationId": "DeliveryController_submitSurvey",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitSurveyResponseDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Submit survey response",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/{token}/review-prompt": {
      "get": {
        "operationId": "DeliveryController_getReviewPrompt",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Get review platform links for this delivery",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/{token}": {
      "get": {
        "operationId": "DeliveryController_getDeliveryByToken",
        "parameters": [
          {
            "name": "token",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveryResponseDto"
                }
              }
            }
          }
        },
        "summary": "Get delivery data by public token",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/delivery/admin/retry-artifact": {
      "post": {
        "operationId": "DeliveryController_retryArtifact",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RetryArtifactDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Retry a failed artifact generation",
        "tags": [
          "Delivery"
        ]
      }
    },
    "/marketplace/jobs": {
      "post": {
        "operationId": "MarketplaceController_createJob",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateMarketplaceJobDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create marketplace listing with service slots",
        "tags": [
          "Marketplace"
        ]
      },
      "get": {
        "operationId": "MarketplaceController_listJobs",
        "parameters": [
          {
            "name": "locationCity",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "locationRegion",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dateFrom",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dateTo",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Object"
              }
            }
          },
          {
            "name": "slotTypes",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Object"
              }
            }
          },
          {
            "name": "budgetMinCents",
            "required": false,
            "in": "query",
            "description": "Budget bounds, in cents, matched against the job's own range.\n\nSERVER-SIDE because the list is paginated. The frontend used to filter\nthese over the current page only, so a filter could empty page 1 while\nmatching jobs sat on page 2 — and the result count kept reporting the\nunfiltered total.",
            "schema": {
              "minimum": 0,
              "type": "number"
            }
          },
          {
            "name": "budgetMaxCents",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "type": "number"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "description": "`(sortBy, sortOrder)` come from `PaginationDto`, redeclared here ONLY to\nnarrow `sortBy` to the columns this list can actually order by — the base\nis an open `string`, and an unvalidated column name reaching Prisma's\n`orderBy` is not something to hand a caller.\n\n`declare` because the base already defines them; a plain redeclaration\nshadows the decorated property and silently drops its validation.\n\nSERVER-SIDE, and for a sharper reason than the filters: sorting one page of\ntwenty is not sorting. \"Highest budget\" returned the highest of whichever\ntwenty jobs the server happened to send — the wrong answer the moment a\ntwenty-first exists, and it looks right because the page IS correctly\nordered within itself.",
            "schema": {
              "type": "string",
              "enum": [
                "requestedDate",
                "createdAt",
                "budgetMaxCents",
                "budgetMinCents"
              ]
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List marketplace jobs feed",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/jobs/mine": {
      "get": {
        "operationId": "MarketplaceController_listMyJobs",
        "parameters": [
          {
            "name": "locationCity",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "locationRegion",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dateFrom",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "dateTo",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Object"
              }
            }
          },
          {
            "name": "slotTypes",
            "required": false,
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/Object"
              }
            }
          },
          {
            "name": "budgetMinCents",
            "required": false,
            "in": "query",
            "description": "Budget bounds, in cents, matched against the job's own range.\n\nSERVER-SIDE because the list is paginated. The frontend used to filter\nthese over the current page only, so a filter could empty page 1 while\nmatching jobs sat on page 2 — and the result count kept reporting the\nunfiltered total.",
            "schema": {
              "minimum": 0,
              "type": "number"
            }
          },
          {
            "name": "budgetMaxCents",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "type": "number"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "description": "`(sortBy, sortOrder)` come from `PaginationDto`, redeclared here ONLY to\nnarrow `sortBy` to the columns this list can actually order by — the base\nis an open `string`, and an unvalidated column name reaching Prisma's\n`orderBy` is not something to hand a caller.\n\n`declare` because the base already defines them; a plain redeclaration\nshadows the decorated property and silently drops its validation.\n\nSERVER-SIDE, and for a sharper reason than the filters: sorting one page of\ntwenty is not sorting. \"Highest budget\" returned the highest of whichever\ntwenty jobs the server happened to send — the wrong answer the moment a\ntwenty-first exists, and it looks right because the page IS correctly\nordered within itself.",
            "schema": {
              "type": "string",
              "enum": [
                "requestedDate",
                "createdAt",
                "budgetMaxCents",
                "budgetMinCents"
              ]
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List marketplace jobs created by current user",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/jobs/{jobId}": {
      "get": {
        "operationId": "MarketplaceController_getJobById",
        "parameters": [
          {
            "name": "jobId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get marketplace job details with all slots",
        "tags": [
          "Marketplace"
        ]
      },
      "delete": {
        "operationId": "MarketplaceController_deleteJob",
        "parameters": [
          {
            "name": "jobId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Delete a marketplace listing",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/jobs/list-project/{projectId}": {
      "post": {
        "operationId": "MarketplaceController_listProjectOnMarketplace",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List an existing project on the marketplace",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/bids": {
      "post": {
        "operationId": "MarketplaceController_createBid",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateMarketplaceBidDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Place or update a bid on a marketplace slot",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/claim": {
      "post": {
        "operationId": "MarketplaceController_claimSlot",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Instantly claim an open marketplace slot",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/threads": {
      "get": {
        "operationId": "MarketplaceController_listMyThreads",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Every marketplace conversation you are party to",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/jobs/{jobId}/thread/{providerUserId}": {
      "get": {
        "operationId": "MarketplaceController_getThread",
        "parameters": [
          {
            "name": "jobId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "providerUserId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "One marketplace conversation",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/jobs/{jobId}/thread/{providerUserId}/messages": {
      "post": {
        "operationId": "MarketplaceController_sendThreadMessage",
        "parameters": [
          {
            "name": "jobId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "providerUserId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendMarketplaceMessageDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Post to a marketplace conversation",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/jobs/{jobId}/thread/{providerUserId}/read": {
      "post": {
        "operationId": "MarketplaceController_markThreadRead",
        "parameters": [
          {
            "name": "jobId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "providerUserId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Mark a marketplace conversation as read",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/bids/{bidId}/counter": {
      "post": {
        "operationId": "MarketplaceController_counterBidAsAgent",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bidId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CounterMarketplaceBidDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Agent proposes a different price on a bid",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/bids/{bidId}/counter-back": {
      "post": {
        "operationId": "MarketplaceController_counterBidAsProvider",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bidId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CounterMarketplaceBidDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Provider proposes a different price back",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/bids/{bidId}/accept-counter": {
      "post": {
        "operationId": "MarketplaceController_acceptCounterOffer",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bidId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Provider accepts the agent's counter-offer, assigning the slot",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/bids/{bidId}/withdraw": {
      "post": {
        "operationId": "MarketplaceController_withdrawBid",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bidId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Withdraw your own bid on a marketplace slot",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/bids/{bidId}/accept": {
      "post": {
        "operationId": "MarketplaceController_acceptBid",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bidId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Accept a bid on a marketplace slot",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/confirmation": {
      "get": {
        "operationId": "MarketplaceController_getBookingConfirmation",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get booking confirmation state for a slot",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/confirmation/confirm": {
      "post": {
        "operationId": "MarketplaceController_confirmBooking",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Confirm booking for a marketplace slot",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/confirmation/decline": {
      "post": {
        "operationId": "MarketplaceController_declineBooking",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeclineMarketplaceBookingConfirmationDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Decline booking confirmation for a marketplace slot",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/deliver": {
      "post": {
        "operationId": "MarketplaceController_deliverSlot",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Provider delivers work for a marketplace slot",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/request-revision": {
      "post": {
        "operationId": "MarketplaceController_requestRevision",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Request revision on a delivered marketplace slot",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/complete": {
      "post": {
        "operationId": "MarketplaceController_completeSlot",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Approve and complete a marketplace slot",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/ratings": {
      "post": {
        "operationId": "MarketplaceController_createRating",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateMarketplaceRatingDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create rating for a completed marketplace slot",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/slots/{slotId}/cancel": {
      "post": {
        "description": "Either side may cancel, and the consequences differ. A provider cancelling releases any escrow in full — the agent is charged nothing. An agent cancelling after the provider has started, or inside the org's cancellation window, may capture the org's configured cancellation fee as compensation. Neither side can cancel once work has been delivered; that is what revisions are for.",
        "operationId": "MarketplaceController_cancelSlot",
        "parameters": [
          {
            "name": "slotId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CancelMarketplaceSlotDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Cancel a marketplace slot",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/jobs/{jobId}/cancel": {
      "post": {
        "description": "Agent-side only. Cancels every slot still in play; delivered and completed slots are left alone. Each slot goes through the same cancellation as a single-slot cancel, so escrow is settled per slot. Reports which slots cancelled and which did not.",
        "operationId": "MarketplaceController_cancelJob",
        "parameters": [
          {
            "name": "jobId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CancelMarketplaceSlotDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Cancel a whole marketplace job",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/providers": {
      "get": {
        "description": "The directory of people who have created a marketplace provider profile — that profile IS the opt-in, so this cannot surface a technician who never joined the marketplace. Returns USER ids, which is what `targetedProviderIds` holds. Deliberately crosses org boundaries (an agent in one org finding a provider in another is the point), so the response is an explicit allow-list of public profile fields: no email, no phone, no memberships.",
        "operationId": "MarketplaceController_listProviders",
        "parameters": [
          {
            "name": "q",
            "required": false,
            "in": "query",
            "description": "Free-text match on the provider's name or headline.",
            "schema": {
              "maxLength": 120,
              "type": "string"
            }
          },
          {
            "name": "specialties",
            "required": false,
            "in": "query",
            "description": "Only providers who list at least one of these as a specialty.\n\nMatches `MarketplaceProviderProfile.specialties`, which is free text the\nprovider typed — so this is a \"says they do this\" filter, not a verified\ncapability. Named `specialties` rather than `slotTypes` for that reason:\ncalling it slot types would imply the two vocabularies are the same and\ninvite someone to filter a job's slots by it.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "verifiedOnly",
            "required": false,
            "in": "query",
            "description": "Restrict to providers whose identity check has passed.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "availableOnly",
            "required": false,
            "in": "query",
            "description": "Restrict to providers currently marking themselves available.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "serviceArea",
            "required": false,
            "in": "query",
            "description": "Only providers whose declared service area contains this text.\n\nHeld back through Phase 3 because `MarketplaceProviderProfile.serviceArea`\nhad zero writers, and a filter over an empty column returns nothing while\nlooking like it worked. The profile wizard now writes it, so it is real.\n\nA CONTAINS MATCH, NOT AN EQUALITY, because the column is a sentence a\nprovider typed: \"Calgary and surrounding areas\" must be found by an agent\nsearching \"Calgary\". It will not find them by \"YYC\", and no amount of\nstring matching would — that is the honest cost of free text, and the\nalternative (a fixed city list) forces every provider to pick the nearest\nlie about where they actually go.\n\nSeparate from `q` on purpose. `q` asks \"anyone matching this word\"; this\nasks \"only people who cover this place\", which is a filter an agent\napplies rather than a search they run.",
            "schema": {
              "maxLength": 120,
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 1,
              "maximum": 200,
              "default": 50,
              "type": "number"
            }
          },
          {
            "name": "offset",
            "required": false,
            "in": "query",
            "schema": {
              "minimum": 0,
              "default": 0,
              "type": "number"
            }
          },
          {
            "name": "createdAfter",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "createdBefore",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortOrder",
            "required": false,
            "in": "query",
            "schema": {
              "default": "desc",
              "allOf": [
                {
                  "$ref": "#/components/schemas/Object"
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Search marketplace providers",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/me/earnings": {
      "get": {
        "operationId": "MarketplaceController_getProviderEarnings",
        "parameters": [
          {
            "name": "page",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get provider earnings summary and transaction history",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/me/provider-profile": {
      "get": {
        "operationId": "MarketplaceController_getMyProviderProfile",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get current user provider profile",
        "tags": [
          "Marketplace"
        ]
      },
      "patch": {
        "operationId": "MarketplaceController_upsertMyProviderProfile",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpsertProviderProfileDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Update current user provider profile",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/marketplace/providers/{userId}/profile": {
      "get": {
        "operationId": "MarketplaceController_getProviderProfile",
        "parameters": [
          {
            "name": "userId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get provider profile by user id",
        "tags": [
          "Marketplace"
        ]
      }
    },
    "/api-keys": {
      "post": {
        "operationId": "ApiKeysController_create",
        "parameters": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateApiKeyDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Create a new API key",
        "tags": [
          "API Keys"
        ]
      },
      "get": {
        "operationId": "ApiKeysController_list",
        "parameters": [],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "List all active API keys for organization",
        "tags": [
          "API Keys"
        ]
      }
    },
    "/api-keys/{id}": {
      "delete": {
        "operationId": "ApiKeysController_revoke",
        "parameters": [
          {
            "name": "id",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Revoke an API key",
        "tags": [
          "API Keys"
        ]
      }
    },
    "/listings/{slugOrToken}": {
      "get": {
        "operationId": "ListingsController_getListingBySlugOrToken",
        "parameters": [
          {
            "name": "slugOrToken",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "unbranded",
            "required": false,
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Get public listing by slug or token",
        "tags": [
          "Listings"
        ]
      }
    },
    "/listings/{slugOrToken}/leads": {
      "post": {
        "operationId": "ListingsController_createListingLead",
        "parameters": [
          {
            "name": "slugOrToken",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateListingLeadDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          }
        },
        "summary": "Submit a listing lead",
        "tags": [
          "Listings"
        ]
      }
    },
    "/listings/projects/{projectId}/status": {
      "get": {
        "operationId": "ListingsController_getListingStatus",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get listing status",
        "tags": [
          "Listings"
        ]
      }
    },
    "/listings/projects/{projectId}/publish": {
      "patch": {
        "operationId": "ListingsController_toggleListingPublished",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Toggle listing published state",
        "tags": [
          "Listings"
        ]
      }
    },
    "/listings/projects/{projectId}/details": {
      "get": {
        "operationId": "ListingsController_getListingDetails",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get listing details",
        "tags": [
          "Listings"
        ]
      },
      "put": {
        "operationId": "ListingsController_upsertListingDetails",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateListingDetailsDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Upsert listing details",
        "tags": [
          "Listings"
        ]
      }
    },
    "/listings/projects/{projectId}/website-config": {
      "get": {
        "operationId": "ListingsController_getWebsiteConfig",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get property-website config",
        "tags": [
          "Listings"
        ]
      },
      "put": {
        "operationId": "ListingsController_upsertWebsiteConfig",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateWebsiteConfigDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Upsert property-website config",
        "tags": [
          "Listings"
        ]
      }
    },
    "/listings/projects/{projectId}/photo-order": {
      "patch": {
        "operationId": "ListingsController_reorderListingPhotos",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReorderListingPhotosDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          }
        },
        "summary": "Arrange the listing's photo order",
        "tags": [
          "Listings"
        ]
      }
    },
    "/listings/projects/{projectId}/website-config/lock": {
      "patch": {
        "operationId": "ListingsController_setCustomerEditsLocked",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LockWebsiteConfigDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Lock or unlock customer website editing",
        "tags": [
          "Listings"
        ]
      }
    },
    "/listings/projects/{projectId}/analytics": {
      "get": {
        "operationId": "ListingsController_getListingAnalytics",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": ""
          },
          "401": {
            "description": "Missing or invalid JWT token"
          },
          "403": {
            "description": "Not a member of the specified organization"
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-org-id": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Get listing analytics",
        "tags": [
          "Listings"
        ]
      }
    },
    "/listings/projects/{projectId}/copy": {
      "post": {
        "operationId": "CopywritingController_generate",
        "parameters": [
          {
            "name": "projectId",
            "required": true,
            "in": "path",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GenerateCopyDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": ""
          }
        },
        "security": [
          {
            "bearer": []
          },
          {
            "x-api-key": []
          }
        ],
        "summary": "Generate AI copy for a property website section",
        "tags": [
          "listings"
        ]
      }
    }
  },
  "info": {
    "title": "Vremly API",
    "description": "Vremly — Real Estate Media Platform API Reference",
    "version": "1.0",
    "contact": {}
  },
  "tags": [
    {
      "name": "App",
      "description": "Health check"
    },
    {
      "name": "Organizations",
      "description": "Organization management"
    },
    {
      "name": "Projects",
      "description": "Project CRUD & workflow"
    },
    {
      "name": "Orders",
      "description": "Order management"
    },
    {
      "name": "Customers",
      "description": "Customer management"
    },
    {
      "name": "Inquiries",
      "description": "Public inquiry forms"
    },
    {
      "name": "Media",
      "description": "Media file management"
    },
    {
      "name": "Packages",
      "description": "Service packages & add-ons"
    },
    {
      "name": "Availability",
      "description": "Scheduling & availability"
    },
    {
      "name": "Delivery",
      "description": "Project delivery & public galleries"
    },
    {
      "name": "Invoices",
      "description": "Invoice management"
    },
    {
      "name": "API Keys",
      "description": "Issuing and revoking organization API keys"
    },
    {
      "name": "Listings",
      "description": "Property websites, listing details, photo order and leads"
    },
    {
      "name": "listings",
      "description": "AI-generated property copy and video scripts. The same /listings surface as above — the two tags differ only by case because the controllers spell them differently"
    },
    {
      "name": "Webhooks",
      "description": "Event subscriptions, deliveries and signature verification"
    },
    {
      "name": "Marketplace",
      "description": "Requests for quote, bids and awarded work"
    }
  ],
  "servers": [],
  "components": {
    "securitySchemes": {
      "bearer": {
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "type": "http"
      },
      "x-org-id": {
        "type": "apiKey",
        "in": "header",
        "name": "x-org-id"
      },
      "x-api-key": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Organization API key. The organization is derived from the key, so x-org-id is not required and is ignored if sent. Scoped: READ covers safe methods, WRITE implies READ, ADMIN covers everything, BULK_IMPORT and WEBHOOKS are narrow."
      }
    },
    "schemas": {
      "CreateTimeOffRequestDto": {
        "type": "object",
        "properties": {
          "type": {
            "type": "object"
          },
          "startDate": {
            "type": "string"
          },
          "endDate": {
            "type": "string"
          },
          "allDay": {
            "type": "boolean",
            "description": "Defaults to true (whole-day). false = a single half day with a time window."
          },
          "startTime": {
            "type": "string"
          },
          "endTime": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "maxLength": 500
          },
          "firstDayBack": {
            "type": "string"
          },
          "shootsInWindow": {
            "type": "string",
            "description": "Shoots already scheduled inside the leave window that need reassigning.",
            "maxLength": 2000
          },
          "deliverablesDue": {
            "type": "string",
            "description": "Deliverables due inside the window and who's covering them.",
            "maxLength": 2000
          },
          "coveredBy": {
            "type": "string",
            "description": "The teammate(s) covering while away.",
            "maxLength": 500
          },
          "coverageConfirmed": {
            "type": "boolean",
            "description": "Requester attests coverage is arranged. When false the request may be\nsubmitted but is NOT self-approvable — it routes to NEEDS_INFO on review."
          },
          "clientThreads": {
            "type": "string",
            "description": "Open client threads that need a hand-off.",
            "maxLength": 2000
          },
          "pmActionItems": {
            "type": "string",
            "description": "Action items the covering PM must own.",
            "maxLength": 2000
          },
          "policyAcknowledged": {
            "type": "boolean",
            "description": "Requester ticked the time-off policy acknowledgment → stamps a timestamp."
          }
        },
        "required": [
          "startDate",
          "endDate"
        ]
      },
      "UpdateTimeOffRequestDto": {
        "type": "object",
        "properties": {}
      },
      "ReviewTimeOffRequestDto": {
        "type": "object",
        "properties": {
          "override": {
            "type": "boolean"
          },
          "reviewNote": {
            "type": "string",
            "maxLength": 500
          }
        }
      },
      "RequestInfoTimeOffDto": {
        "type": "object",
        "properties": {
          "note": {
            "type": "string",
            "maxLength": 500
          }
        }
      },
      "UpdateTimeOffPolicyDto": {
        "type": "object",
        "properties": {
          "role": {
            "type": "object",
            "enum": [
              "OWNER",
              "ADMIN",
              "TECHNICIAN",
              "EDITOR",
              "PROJECT_MANAGER"
            ]
          },
          "annualAllowanceDays": {
            "type": "number",
            "minimum": 0,
            "maximum": 366
          }
        },
        "required": [
          "role",
          "annualAllowanceDays"
        ]
      },
      "BlackoutWindowDto": {
        "type": "object",
        "properties": {
          "startMonth": {
            "type": "number",
            "minimum": 1,
            "maximum": 12
          },
          "endMonth": {
            "type": "number",
            "minimum": 1,
            "maximum": 12
          }
        },
        "required": [
          "startMonth",
          "endMonth"
        ]
      },
      "BlackoutWindowsDto": {
        "type": "object",
        "properties": {
          "restricted": {
            "maxItems": 24,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BlackoutWindowDto"
            }
          },
          "encouraged": {
            "maxItems": 24,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BlackoutWindowDto"
            }
          }
        }
      },
      "UpdateTimeOffSettingsDto": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "noticeDaysVacation": {
            "type": "number",
            "minimum": 0,
            "maximum": 365
          },
          "noticeDaysPersonal": {
            "type": "number",
            "minimum": 0,
            "maximum": 365
          },
          "slaBusinessDays": {
            "type": "number",
            "minimum": 0,
            "maximum": 30
          },
          "maxSelfApproveDays": {
            "type": "number",
            "minimum": 0,
            "maximum": 60
          },
          "approverRoles": {
            "maxItems": 7,
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "blackoutWindows": {
            "$ref": "#/components/schemas/BlackoutWindowsDto"
          }
        }
      },
      "RegisterDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string"
          },
          "password": {
            "type": "string",
            "minLength": 6
          },
          "accountType": {
            "type": "object",
            "description": "User-facing intent for the account.\nOnly AGENT and PROVIDER are user-facing in the UI."
          },
          "companyAccessCode": {
            "type": "string",
            "maxLength": 120
          }
        },
        "required": [
          "email",
          "name",
          "password",
          "accountType"
        ]
      },
      "OAuthLoginDto": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string"
          },
          "accountType": {
            "type": "object"
          },
          "name": {
            "type": "string"
          },
          "whitelabelOrgId": {
            "type": "string"
          }
        },
        "required": [
          "token",
          "accountType"
        ]
      },
      "GoogleSsoFallbackTicketDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string"
          },
          "stateToken": {
            "type": "string"
          }
        },
        "required": [
          "stateToken"
        ]
      },
      "OnboardingRegisterDto": {
        "type": "object",
        "properties": {
          "otpToken": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string"
          },
          "password": {
            "type": "string",
            "minLength": 8
          },
          "accountType": {
            "type": "object"
          },
          "inviteCode": {
            "type": "string"
          },
          "useCases": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "companyAccessCode": {
            "type": "string",
            "maxLength": 120
          },
          "whitelabelOrgId": {
            "type": "string",
            "description": "When the user signs up on a whitelabel custom domain, this is the\norgId of that company. The backend creates an OrganizationCustomer\nrecord so the user lands in that company's customer portal."
          }
        },
        "required": [
          "otpToken",
          "email",
          "name",
          "password",
          "accountType"
        ]
      },
      "CompleteOnboardingDto": {
        "type": "object",
        "properties": {
          "accountType": {
            "type": "object"
          },
          "useCases": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "inviteCode": {
            "type": "string"
          },
          "companyName": {
            "type": "string",
            "maxLength": 120
          },
          "companyAccessCode": {
            "type": "string",
            "maxLength": 120
          },
          "whitelabelOrgId": {
            "type": "string",
            "description": "When signing up from a whitelabel workspace URL, auto-associate with this org"
          }
        },
        "required": [
          "accountType"
        ]
      },
      "SendOtpDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "orgId": {
            "type": "string",
            "description": "Optional org ID for whitelabel branding on the OTP email"
          },
          "purpose": {
            "type": "object",
            "description": "Purpose the code is issued for. Scopes how it can later be redeemed:\n'signup' (default) rejects already-registered emails; 'login_2fa',\n'password_reset' and 'returning_customer_setup' allow them. A code minted\nfor one purpose cannot be redeemed by another flow.",
            "enum": [
              "signup",
              "login_2fa",
              "password_reset",
              "returning_customer_setup"
            ]
          }
        },
        "required": [
          "email"
        ]
      },
      "VerifyOtpDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "code": {
            "type": "string",
            "minLength": 6,
            "maxLength": 6
          }
        },
        "required": [
          "email",
          "code"
        ]
      },
      "CreateTierConfigDto": {
        "type": "object",
        "properties": {
          "tierSlug": {
            "type": "string",
            "maxLength": 32
          },
          "displayName": {
            "type": "string",
            "maxLength": 80
          },
          "thresholdCents": {
            "type": "number",
            "minimum": 0
          },
          "cashbackRateBps": {
            "type": "number",
            "minimum": 0
          },
          "perks": {
            "type": "object"
          },
          "color": {
            "type": "string"
          },
          "icon": {
            "type": "string"
          },
          "isEnabled": {
            "type": "boolean"
          },
          "displayOrder": {
            "type": "number"
          }
        },
        "required": [
          "tierSlug",
          "displayName",
          "thresholdCents",
          "cashbackRateBps"
        ]
      },
      "UpdateTierConfigDto": {
        "type": "object",
        "properties": {
          "displayName": {
            "type": "string"
          },
          "thresholdCents": {
            "type": "number",
            "minimum": 0
          },
          "cashbackRateBps": {
            "type": "number",
            "minimum": 0
          },
          "perks": {
            "type": "object"
          },
          "color": {
            "type": "string"
          },
          "icon": {
            "type": "string"
          },
          "isEnabled": {
            "type": "boolean"
          },
          "displayOrder": {
            "type": "number"
          }
        }
      },
      "BatchTierUpdateItem": {
        "type": "object",
        "properties": {
          "tierSlug": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "thresholdCents": {
            "type": "number",
            "minimum": 0
          },
          "cashbackRateBps": {
            "type": "number",
            "minimum": 0
          },
          "perks": {
            "type": "object"
          },
          "color": {
            "type": "string"
          },
          "icon": {
            "type": "string"
          },
          "isEnabled": {
            "type": "boolean"
          },
          "displayOrder": {
            "type": "number"
          }
        },
        "required": [
          "tierSlug"
        ]
      },
      "BatchTierUpdateDto": {
        "type": "object",
        "properties": {
          "tiers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BatchTierUpdateItem"
            }
          }
        },
        "required": [
          "tiers"
        ]
      },
      "ToggleLoyaltyDto": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          }
        },
        "required": [
          "enabled"
        ]
      },
      "ToggleStreakDto": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          }
        },
        "required": [
          "enabled"
        ]
      },
      "StreakMilestoneItem": {
        "type": "object",
        "properties": {
          "days": {
            "type": "number",
            "minimum": 1
          },
          "kind": {
            "type": "object",
            "enum": [
              "currency_bonus",
              "discount_next_order_pct",
              "discount_next_order_cents",
              "free_add_on",
              "free_package",
              "perk_template"
            ]
          },
          "bonusVX": {
            "type": "number",
            "minimum": 0
          },
          "percentBps": {
            "type": "number",
            "minimum": 0,
            "maximum": 10000
          },
          "cents": {
            "type": "number",
            "minimum": 0
          },
          "addOnId": {
            "type": "string"
          },
          "packageId": {
            "type": "string"
          },
          "perkTemplateKey": {
            "type": "string"
          },
          "config": {
            "type": "object"
          },
          "expiresAfterDays": {
            "type": "number",
            "minimum": 0
          }
        },
        "required": [
          "days",
          "kind"
        ]
      },
      "UpdateStreakConfigDto": {
        "type": "object",
        "properties": {
          "streakBaseRewardVX": {
            "type": "number",
            "minimum": 0
          },
          "streakDailyBonusVX": {
            "type": "number",
            "minimum": 0
          },
          "streakVxPerCent": {
            "type": "number",
            "minimum": 1
          },
          "streakGraceDays": {
            "type": "number",
            "minimum": 0
          },
          "streakGraceWindowDays": {
            "type": "number",
            "minimum": 1
          },
          "streakMilestones": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StreakMilestoneItem"
            }
          },
          "streakCurrencyName": {
            "type": "string",
            "maxLength": 40
          },
          "streakCurrencyShortName": {
            "type": "string",
            "maxLength": 12
          }
        }
      },
      "ManualAdjustmentDto": {
        "type": "object",
        "properties": {
          "customerId": {
            "type": "string"
          },
          "amountCents": {
            "type": "number"
          },
          "reason": {
            "type": "string"
          }
        },
        "required": [
          "customerId",
          "amountCents",
          "reason"
        ]
      },
      "StandingOverrideDto": {
        "type": "object",
        "properties": {
          "customerId": {
            "type": "string"
          },
          "bonusSpendCents": {
            "type": "number",
            "description": "GRANTED PROGRESS, in cents, as a delta against earned lifetime spend.\nPositive grants, negative corrects, 0 removes. Deliberately NOT `@Min(0)`:\na negative grant is how an operator walks back progress handed out in\nerror. The effective figure is floored at zero where it is read."
          },
          "tierOverrideSlug": {
            "type": "string",
            "nullable": true,
            "description": "GRANTED TIER pin. Must be an ENABLED tier slug for this org — the service\nchecks that against LoyaltyTierConfig, because class-validator cannot know\none org's tier list. `null` removes the pin."
          },
          "reason": {
            "type": "string",
            "description": "Required, and load-bearing rather than decorative: this changes the rate\nthe customer earns cashback at on every future order, and loyalty credit is\nredeemable against an invoice. The service trims and re-checks it, because\na single space passes `@IsString()`.",
            "maxLength": 500
          }
        },
        "required": [
          "customerId",
          "reason"
        ]
      },
      "ClearStandingOverrideDto": {
        "type": "object",
        "properties": {
          "customerId": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "maxLength": 500
          }
        },
        "required": [
          "customerId",
          "reason"
        ]
      },
      "GrantRewardDto": {
        "type": "object",
        "properties": {
          "customerId": {
            "type": "string",
            "description": "An existing OrganizationCustomer in the caller's org. The service re-binds\nit to `orgId` before touching anything — a bare id would otherwise let one\norg mint a loyalty account against another org's customer.",
            "maxLength": 64
          },
          "email": {
            "type": "string",
            "description": "Grant to an email address instead. A recipient with no account is a\nFIRST-CLASS case, not an edge case: the service creates (or adopts) the\ncustomer row, mints a pending CUSTOMER invitation so the notification email\nhas a real signup link, and the reward is waiting when they arrive.",
            "maxLength": 320,
            "format": "email"
          },
          "customerName": {
            "type": "string",
            "description": "Display name for a brand-new customer row created by the email arm. Ignored\nwhen the address already belongs to a customer — an existing person's name\nis theirs, and a grant is not the place to rename them.",
            "maxLength": 200
          },
          "kind": {
            "type": "object",
            "enum": [
              "discount_next_order_pct",
              "discount_next_order_cents",
              "free_add_on",
              "free_package",
              "perk_template"
            ]
          },
          "percentBps": {
            "type": "number",
            "description": "discount_next_order_pct — basis points. 10000 = 100% off.",
            "minimum": 1,
            "maximum": 10000
          },
          "cents": {
            "type": "number",
            "description": "discount_next_order_cents — flat cents off the next order.",
            "minimum": 1,
            "maximum": 1000000
          },
          "addOnId": {
            "type": "string",
            "description": "free_add_on — PackageAddOn.id. The service checks it belongs to this org.",
            "maxLength": 64
          },
          "packageId": {
            "type": "string",
            "description": "free_package — ServicePackage.id.\n\nA free_package reward is 100% off THE MATCHING PACKAGE'S LINE in an order.\nIt is not, and must never become, a blanket free order: if the customer's\norder does not contain this package, the reward is simply not applicable\nand the checkout says so. That is why the id is required rather than\noptional — an entitlement with no packageId cannot be matched to a line,\nand \"matches nothing\" would have to be resolved either by refusing every\norder or by discounting all of them.",
            "maxLength": 64
          },
          "perkTemplateKey": {
            "type": "string",
            "description": "perk_template — a key from perk-templates.ts.",
            "maxLength": 64
          },
          "config": {
            "type": "object",
            "description": "perk_template — the template's own config blob, shape defined per template."
          },
          "expiresAt": {
            "type": "string",
            "description": "Absolute expiry instant, ISO-8601. OPTIONAL — omitting both this and\n`expiresAfterDays` means the reward never expires, which is a supported\noutcome rather than a missing value."
          },
          "expiresAfterDays": {
            "type": "number",
            "description": "TTL in days from the moment of the grant. Mutually exclusive with `expiresAt`.",
            "minimum": 1,
            "maximum": 3650
          },
          "note": {
            "type": "string",
            "description": "Why this was granted. Not required — unlike a manual credit adjustment,\nwhich moves money against a balance — but it is what the audit line reads\nsix months later, so the UI should ask for it.",
            "maxLength": 500
          },
          "displayName": {
            "type": "string",
            "description": "Override the auto-generated label (\"Free Premium Package\"). The default is\ncomputed from the catalog at grant time and frozen onto the row, so a\nrenamed package never blanks a customer's pending reward.",
            "maxLength": 120
          }
        },
        "required": [
          "kind"
        ]
      },
      "RevokeEntitlementDto": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "maxLength": 500
          }
        },
        "required": [
          "reason"
        ]
      },
      "CreateEarningRuleDto": {
        "type": "object",
        "properties": {
          "trigger": {
            "type": "object"
          },
          "rewardKind": {
            "type": "object"
          },
          "valueBps": {
            "type": "number",
            "minimum": 0,
            "maximum": 100000
          },
          "flatBonusCents": {
            "type": "number",
            "minimum": 0
          },
          "displayName": {
            "type": "string",
            "maxLength": 80
          },
          "description": {
            "type": "string",
            "maxLength": 280
          },
          "conditions": {
            "type": "object"
          },
          "isEnabled": {
            "type": "boolean"
          },
          "displayOrder": {
            "type": "number"
          }
        },
        "required": [
          "trigger",
          "rewardKind",
          "valueBps",
          "flatBonusCents",
          "displayName"
        ]
      },
      "UpdateEarningRuleDto": {
        "type": "object",
        "properties": {
          "trigger": {
            "type": "object"
          },
          "rewardKind": {
            "type": "object"
          },
          "valueBps": {
            "type": "number",
            "minimum": 0,
            "maximum": 100000
          },
          "flatBonusCents": {
            "type": "number",
            "minimum": 0
          },
          "displayName": {
            "type": "string",
            "maxLength": 80
          },
          "description": {
            "type": "string",
            "maxLength": 280
          },
          "conditions": {
            "type": "object"
          },
          "isEnabled": {
            "type": "boolean"
          },
          "displayOrder": {
            "type": "number"
          }
        }
      },
      "UpdateCurrencyConfigDto": {
        "type": "object",
        "properties": {
          "loyaltyCurrencyName": {
            "type": "string",
            "maxLength": 40
          },
          "loyaltyCurrencyShortName": {
            "type": "string",
            "maxLength": 12
          },
          "loyaltyPointsPerCent": {
            "type": "number",
            "minimum": 1
          }
        }
      },
      "UpdateReferralConfigDto": {
        "type": "object",
        "properties": {
          "referralEnabled": {
            "type": "boolean"
          },
          "affiliateProgramEnabled": {
            "type": "boolean"
          },
          "referrerRewardCents": {
            "type": "number",
            "minimum": 0
          },
          "refereeRewardCents": {
            "type": "number",
            "minimum": 0
          },
          "referralRewardTrigger": {
            "type": "object"
          },
          "referralTerms": {
            "type": "string",
            "maxLength": 2000
          },
          "affiliateProgramName": {
            "type": "string",
            "maxLength": 120
          },
          "affiliateResources": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                }
              },
              "required": [
                "label",
                "url"
              ]
            }
          },
          "affiliateAssets": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "imageUrl": {
                  "type": "string"
                }
              },
              "required": [
                "title",
                "url"
              ]
            }
          }
        }
      },
      "AssignedProjectPreviewDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "address": {
            "type": "string"
          }
        },
        "required": [
          "id"
        ]
      },
      "NotificationResponseDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "object"
          },
          "category": {
            "type": "object",
            "description": "Raw backend category for this notification's type. Lets clients group /\n filter the inbox without re-deriving the taxonomy from the type."
          },
          "orgId": {
            "type": "string"
          },
          "orgName": {
            "type": "string"
          },
          "orgType": {
            "type": "object"
          },
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          },
          "readAt": {
            "format": "date-time",
            "type": "string",
            "nullable": true
          },
          "invitationId": {
            "type": "string"
          },
          "shareInviteId": {
            "type": "string"
          },
          "role": {
            "type": "object"
          },
          "projectId": {
            "type": "string"
          },
          "projectAddress": {
            "type": "string"
          },
          "assignedRole": {
            "type": "object"
          },
          "assignedCount": {
            "type": "number"
          },
          "assignedProjects": {
            "description": "Preview of the projects in the batch (capped server-side, newest first).",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AssignedProjectPreviewDto"
            }
          },
          "messagePreview": {
            "type": "string"
          },
          "messageChannel": {
            "type": "object"
          },
          "senderName": {
            "type": "string",
            "description": "Display name of the user who sent the message — used by the\n inbox row to surface \"From <senderName>\" instead of a generic\n \"New message\" headline."
          },
          "mentioned": {
            "type": "boolean",
            "description": "True when THIS recipient was @mentioned in the message body. Lets clients\n render \"<senderName> mentioned you\" and keep the row out of the\n \"N new messages\" per-project rollup — an unmissable direct callout rather\n than one more line in the chat firehose."
          },
          "messageId": {
            "type": "string",
            "description": "The chat message this notification is about. Needed to deep-link to the\n specific message (scroll/highlight), not just the project's chat tab."
          },
          "approverName": {
            "type": "string"
          },
          "deliveryToken": {
            "type": "string"
          },
          "marketplaceJobId": {
            "type": "string"
          },
          "marketplaceJobTitle": {
            "type": "string"
          },
          "marketplaceConfirmationStatus": {
            "type": "string"
          },
          "marketplaceDeclineReason": {
            "type": "string"
          },
          "bulkImportTaskId": {
            "type": "string"
          },
          "bulkImportEntityType": {
            "type": "object"
          },
          "bulkImportTotal": {
            "type": "number"
          },
          "bulkImportCreated": {
            "type": "number"
          },
          "bulkImportSkipped": {
            "type": "number"
          },
          "bulkImportErrors": {
            "type": "number"
          },
          "bulkImportSampleErrors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "bulkImportFailed": {
            "type": "boolean"
          },
          "ratingScore": {
            "type": "number"
          },
          "ratingRole": {
            "type": "string"
          },
          "raterName": {
            "type": "string"
          },
          "cartId": {
            "type": "string",
            "description": "For BOOKING_ABANDONED — the `PendingOrder.id` of the unfinished cart.\n\nFlattened out of the payload rather than left in it for the same reason\n`assignedCount` is: a client that reads only `projectId` renders this row\nwith a dead \"View Project\" button, because a PendingOrder has no Project by\nconstruction. This id is the `?resume=` target the booking flow, the resume\nscreen and the checkout-cancel page already use — clients deep-link to\n`/booking?resume=<cartId>` and must not invent a second address for it."
          },
          "videoScriptSetId": {
            "type": "string",
            "description": "For VIDEO_SCRIPTS_READY — the `VideoScriptSet.id` this row announces.\n\n`projectId` is present on this row too (the scripts belong to a listing),\nso unlike `cartId` this is not what rescues the row from being untappable.\nIt is what makes the tap land on the RIGHT thing: a listing accumulates a\nset per regeneration, and an agent who asked for a rewrite and then opened\na week-old notification must get the set that notification was about, not\nwhatever is newest. Flattened out of the payload for the same reason every\nfield above is — clients read the DTO, not the raw payload blob."
          }
        },
        "required": [
          "id",
          "type",
          "orgId",
          "orgName",
          "orgType",
          "createdAt"
        ]
      },
      "RegisterPushTokenDto": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "description": "The push token. For platform=expo this is an ExponentPushToken[…] string. For platform=apns this is the hex-encoded APNs device token from application(_:didRegisterForRemoteNotificationsWithDeviceToken:)."
          },
          "platform": {
            "type": "string",
            "enum": [
              "expo",
              "apns",
              "ios",
              "android"
            ],
            "description": "Token format. 'expo' = Expo push service (RN app). 'apns' = direct APNs (SwiftUI app, see PRD §6.5). 'ios' / 'android' are accepted for backwards compatibility and treated as expo-format tokens."
          },
          "deviceName": {
            "type": "string"
          },
          "bundleId": {
            "type": "string",
            "description": "iOS CFBundleIdentifier (e.g. 'com.vremly.agent' or 'com.vremly.virtualxposure'). Required for platform=apns so the multi-tenant APNs sender picks the right auth key + team. Expo tokens omit it."
          },
          "environment": {
            "type": "string",
            "description": "APNs environment the device token was issued by: 'sandbox' for Debug-signed builds (aps-environment=development) or 'production' for App-Store / TestFlight builds. Apple's production gateway returns 200 OK for sandbox tokens but the push silently drops, so the dispatcher must send to the gateway that matches the token. Optional — when omitted the dispatcher falls back to its dual-gateway retry. Send \"sandbox\" from Swift Debug builds via `#if DEBUG`."
          }
        },
        "required": [
          "token"
        ]
      },
      "WebPushKeysDto": {
        "type": "object",
        "properties": {
          "p256dh": {
            "type": "string",
            "maxLength": 255,
            "description": "base64url `getKey('p256dh')` — the subscription's public key."
          },
          "auth": {
            "type": "string",
            "maxLength": 255,
            "description": "base64url `getKey('auth')` — the subscription's auth secret."
          }
        },
        "required": [
          "p256dh",
          "auth"
        ]
      },
      "WebPushSubscribeDto": {
        "type": "object",
        "properties": {
          "endpoint": {
            "type": "string",
            "maxLength": 2048,
            "description": "The push service's delivery URL for this browser. THE identity of the subscription — the row is upserted on it. Must be https and must not resolve to an internal host (see isAcceptablePushEndpoint)."
          },
          "keys": {
            "$ref": "#/components/schemas/WebPushKeysDto"
          },
          "userAgent": {
            "type": "string",
            "maxLength": 512,
            "description": "Browser UA at registration. Display only; never routed on."
          },
          "origin": {
            "type": "string",
            "maxLength": 255,
            "description": "The origin the service worker was registered from, e.g. \"https://app.vremly.com\" or a tenant's own domain. Required — a subscription with an unknown origin cannot be attributed to a tenant."
          }
        },
        "required": [
          "endpoint",
          "keys",
          "origin"
        ]
      },
      "WebPushUnsubscribeDto": {
        "type": "object",
        "properties": {
          "endpoint": {
            "type": "string",
            "maxLength": 2048,
            "description": "The endpoint to stop sending to."
          }
        },
        "required": [
          "endpoint"
        ]
      },
      "UpdateNotificationPreferenceDto": {
        "type": "object",
        "properties": {
          "orgId": {
            "type": "string"
          },
          "category": {
            "type": "object"
          },
          "enabled": {
            "type": "boolean"
          }
        },
        "required": [
          "orgId",
          "category",
          "enabled"
        ]
      },
      "UpdateNotificationEventPreferenceDto": {
        "type": "object",
        "properties": {
          "orgId": {
            "type": "string"
          },
          "type": {
            "type": "object"
          },
          "enabled": {
            "type": "boolean"
          }
        },
        "required": [
          "orgId",
          "type",
          "enabled"
        ]
      },
      "MarkPromoOnboardingShownDto": {
        "type": "object",
        "properties": {
          "orgId": {
            "type": "string"
          }
        },
        "required": [
          "orgId"
        ]
      },
      "SetSlackRoutesDto": {
        "type": "object",
        "properties": {
          "routes": {
            "type": "object"
          }
        },
        "required": [
          "routes"
        ]
      },
      "Object": {
        "type": "object",
        "properties": {}
      },
      "PreviewBrandedEmailDto": {
        "type": "object",
        "properties": {}
      },
      "UpdateProfileDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "avatarUrl": {
            "type": "string"
          },
          "preferredLanguage": {
            "type": "string"
          },
          "timezone": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "shareOrdersWithTeam": {
            "type": "boolean",
            "description": "AGENT TEAM — \"share my orders with my team\", DEFAULT true (see\n`User.shareOrdersWithTeam` in schema.prisma for the full semantics).\n\nSelf-only, exactly like the birthday fields above and for the same reason:\nthis DTO backs PATCH /users/me and the admin-facing UpdateUserDto\ndeliberately does NOT carry it, so an org owner cannot flip a colleague's\nor a customer's sharing preference for them.\n\nMUST be declared here. main.ts runs class-validator with `whitelist: true`,\nso an undeclared key is silently stripped and the toggle would appear to\nsave and then read back unchanged, with no error anywhere."
          },
          "birthdayMonth": {
            "type": "number",
            "nullable": true,
            "description": "OPTIONAL birthday — month + day, with the year optional on top. Only ever\nsettable by the person themselves: this DTO backs PATCH /users/me, and the\nadmin-facing UpdateUserDto deliberately does NOT carry these fields, so an\norg owner cannot write a colleague's or customer's birthday.\n\nSend all three as `null` to clear it.\n\nNOTE the decorator asymmetry, which is intentional:\n - `birthdayMonth` carries the cross-field @Validate and NO @IsOptional, so\n   the whole-triple rule runs on every request — including one that sends\n   only a day, or only a year, both of which must be rejected.\n - `birthdayDay` / `birthdayYear` carry @IsOptional @IsInt purely so a\n   non-numeric value is reported as such; their real validation (range,\n   month length, leap year) happens in the constraint above."
          },
          "birthdayDay": {
            "type": "number",
            "nullable": true
          },
          "birthdayYear": {
            "type": "number",
            "nullable": true
          }
        }
      },
      "AccountActionDto": {
        "type": "object",
        "properties": {
          "password": {
            "type": "string",
            "minLength": 1
          },
          "confirmationText": {
            "type": "string"
          }
        }
      },
      "UpdateUseCasesDto": {
        "type": "object",
        "properties": {
          "useCases": {
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        },
        "required": [
          "useCases"
        ]
      },
      "UpdateOnboardingProfileDto": {
        "type": "object",
        "properties": {
          "services": {
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 64
            }
          },
          "listingsPerMonth": {
            "type": "string",
            "maxLength": 64
          },
          "market": {
            "type": "string",
            "maxLength": 120
          }
        }
      },
      "UpdateNotificationPrefsDto": {
        "type": "object",
        "properties": {
          "emailNotifications": {
            "type": "boolean",
            "description": "Master email kill-switch. When false, EmailService.shouldSendEmail\nsuppresses every per-user-gated email category except the legally/\nfinancially exempt ones (see EMAIL_MASTER_EXEMPT_KEYS). This is the\nfield the \"Email Notifications\" UI toggle binds to."
          },
          "emailNewOrder": {
            "type": "boolean"
          },
          "emailOrderConfirmed": {
            "type": "boolean"
          },
          "emailProjectAssigned": {
            "type": "boolean"
          },
          "emailStatusChange": {
            "type": "boolean"
          },
          "emailDeliveryReady": {
            "type": "boolean"
          },
          "emailApprovalChange": {
            "type": "boolean"
          },
          "emailNewMessage": {
            "type": "boolean"
          },
          "emailInvoice": {
            "type": "boolean"
          },
          "emailPropertyAnalytics": {
            "type": "boolean",
            "description": "Weekly per-listing performance report. Needed here as well as on the model:\nValidationPipe runs `forbidNonWhitelisted`, so a field absent from this DTO\nis a 400, not a silent drop — the settings page could not turn this off at\nall without it."
          },
          "emailDigestFrequency": {
            "type": "string",
            "enum": [
              "instant",
              "daily",
              "off"
            ]
          },
          "smsNotifications": {
            "type": "boolean"
          },
          "phoneNumber": {
            "type": "string"
          },
          "smsProjectAssigned": {
            "type": "boolean"
          },
          "smsStatusChange": {
            "type": "boolean"
          },
          "smsNewOrder": {
            "type": "boolean"
          },
          "smsNewMessage": {
            "type": "boolean"
          },
          "smsDeliveryReady": {
            "type": "boolean"
          },
          "smsApprovalChange": {
            "type": "boolean"
          },
          "smsInvoice": {
            "type": "boolean"
          }
        }
      },
      "SwitchAccountTypeDto": {
        "type": "object",
        "properties": {
          "accountType": {
            "type": "object"
          },
          "companyName": {
            "type": "string",
            "maxLength": 120
          },
          "companyAccessCode": {
            "type": "string",
            "maxLength": 120
          }
        },
        "required": [
          "accountType"
        ]
      },
      "CreateUserDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string"
          },
          "password": {
            "type": "string",
            "minLength": 6
          },
          "accountType": {
            "type": "object"
          }
        },
        "required": [
          "email",
          "name",
          "password",
          "accountType"
        ]
      },
      "UpdateUserDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string"
          },
          "avatarUrl": {
            "type": "string"
          },
          "accountType": {
            "type": "object"
          },
          "preferredLanguage": {
            "type": "string"
          },
          "timezone": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          }
        }
      },
      "UpdateBusyRulesDto": {
        "type": "object",
        "properties": {
          "allDayFree": {
            "type": "boolean",
            "nullable": true,
            "description": "All-day entries the calendar publishes as FREE (public holidays)."
          },
          "allDayBusy": {
            "type": "boolean",
            "nullable": true,
            "description": "All-day entries the calendar publishes as BUSY (vacation / out-of-office)."
          },
          "tentative": {
            "type": "boolean",
            "nullable": true,
            "description": "ALL-DAY entries the calendar publishes as TENTATIVE. A TIMED tentative\n entry always blocks and is deliberately not configurable — see\n classifyExternalEvent, whose all-day gate runs first."
          }
        }
      },
      "ConnectIcsDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "minLength": 8,
            "maxLength": 2048
          },
          "label": {
            "type": "string",
            "maxLength": 120
          }
        },
        "required": [
          "url"
        ]
      },
      "BusyBlockDto": {
        "type": "object",
        "properties": {
          "start": {
            "type": "string"
          },
          "end": {
            "type": "string"
          }
        },
        "required": [
          "start",
          "end"
        ]
      },
      "DeviceBusyDto": {
        "type": "object",
        "properties": {
          "blocks": {
            "maxItems": 2000,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BusyBlockDto"
            }
          }
        },
        "required": [
          "blocks"
        ]
      },
      "CreateProjectMediaDto": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "cdnUrl": {
            "type": "string"
          },
          "externalUrl": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "size": {
            "type": "number"
          },
          "type": {
            "type": "object"
          },
          "isRawUpload": {
            "type": "boolean"
          },
          "rawFolder": {
            "type": "string"
          }
        },
        "required": [
          "filename",
          "size",
          "type"
        ]
      },
      "ImportProjectMediaDto": {
        "type": "object",
        "properties": {
          "sourceProjectId": {
            "type": "string",
            "description": "The job the files are copied FROM. Must be a different job, same org."
          },
          "mediaIds": {
            "description": "Media ids on the source job. Re-validated server-side against that project\nand against the importable-kind filter — this list is a request, not a\ngrant.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "sourceProjectId",
          "mediaIds"
        ]
      },
      "ImportDriveFilesDto": {
        "type": "object",
        "properties": {
          "fileIds": {
            "minItems": 1,
            "maxItems": 40,
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rawFolder": {
            "type": "string",
            "description": "Optional label grouping these under a folder in the Raw / Source bucket,\nmatching the folder the operator was browsing. Cosmetic grouping only — the\nsame field a technician's folder upload already sets."
          }
        },
        "required": [
          "fileIds"
        ]
      },
      "CreateMessageDto": {
        "type": "object",
        "properties": {
          "content": {
            "type": "string"
          },
          "channel": {
            "type": "object"
          },
          "thread": {
            "type": "string",
            "nullable": true
          },
          "mentionUserIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "content"
        ]
      },
      "CreateProjectDto": {
        "type": "object",
        "properties": {
          "addressLine1": {
            "type": "string"
          },
          "addressLine2": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "notes": {
            "type": "string"
          },
          "scheduledTime": {
            "type": "string"
          },
          "customerId": {
            "type": "string"
          },
          "projectManagerId": {
            "type": "string"
          },
          "technicianId": {
            "type": "string"
          },
          "editorId": {
            "type": "string"
          },
          "mediaTypes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "packageId": {
            "type": "string"
          },
          "estimatedDuration": {
            "type": "number",
            "description": "Total shoot length in minutes for the initial appointment, as computed by\nthe create form (package + add-ons + any manually-added extra time).\n\nADVISORY, NEVER AUTHORITATIVE. The server resolves the package + add-ons\nitself and that is the contract; this number is honoured only when it\nEXCEEDS that resolved length — the form's \"Add extra time\" control is\nstrictly additive and is the only thing that produces such a number — and is\nthen recorded as an explicit MANUAL override attributed to the caller. An\nequal or SHORTER number never overrules the package: no client can express a\ndeliberate shortening at booking, so a short number is a stale one.\n\nWhen the catalogue carries no duration at all, this number is honoured ONLY\nif the caller also sets {@link estimatedDurationIsManual}. Otherwise it is\nignored and the shoot is left with no contracted length, which keeps the\npayroll flag alive. See `shoot-duration.util.decideBookingDuration`."
          },
          "estimatedDurationIsManual": {
            "type": "boolean",
            "description": "\"A human typed `estimatedDuration` into an otherwise-empty field.\"\n\nThe ONLY thing that lets a client-supplied number establish a length when\nthe catalogue is silent. It exists because the number alone cannot carry its\nown provenance: every client used to send a FABRICATED value here — the\nstaff form pre-filled 120 and snapped back to 120 when cleared, the agent\nflow held a `useState(120)` nothing ever updated — and the backend recorded\nthose as `contractedDurationSource = BOOKING`, i.e. \"the customer bought two\nhours\", which then silenced the payroll flag for a shoot whose length nobody\nhad established.\n\nA number sent WITHOUT this flag is treated as possibly-defaulted and is\ndropped when nothing else establishes a length, so an old or cached client\ndegrades to a flagged shoot rather than to an invented contract. Honoured\nvalues are stamped MANUAL, never BOOKING.\n\nWHO SENDS IT: `JobRequestForm` → `JobManagementContext.createJob` →\n`api.projects.create`, and only on the branch where the selected services\ncarry NO duration of their own — the branch that renders the free-text\n\"minutes\" input. Whatever sits in that input got there by a human act (typed\nin, or dragged as a span on the calendar and carried in through\n`initialValues`); nothing seeds it, and clearing it leaves it empty. When the\ncatalogue does answer, the field is read-only and the flag is NOT sent: the\nPM's +15m / +30m / +60m top-up is already covered by the longer-than-\ncatalogue rule, which records the same MANUAL stamp."
          },
          "selectedAddOnIds": {
            "description": "Add-on ids selected in the manual \"New Project\" flow. Persisted by\nprojects.service.create (which must read dto.selectedAddOnIds) so the\nadd-on picks drive the initial invoice — mirroring the update() path that\nalready accepts this field. Mirrors CreatePublicOrderDto.addOnIds.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "bookingAnswers": {
            "description": "Booking-question answers collected in the staff \"New Project\" flow, matching\nthe questions a customer answers during self-booking. Persisted structured\non Project.bookingAnswers (NOT concatenated into notes) so the \"Booking\ndetails\" section renders them — mirrors CreateOrderDto/CreatePublicOrderDto.",
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "discountId": {
            "type": "string",
            "description": "THE PROMOTIONAL INCENTIVE A COORDINATOR ATTACHES WHILE BOOKING BY HAND.\n\n`JobRequestForm` → `JobManagementContext.createJob` → `api.projects.create`\nis the ONLY client, and this is the door the staff \"New Project\" dialog\nactually goes through (not `POST /orders/create`).\n\nAn `OrgDiscount` by row id (a named markdown the org configured) or by\ncoupon `code` — send EXACTLY ONE. Both, or neither, is refused as\n`ambiguous_request` and the project is still created.\n\n── NO AMOUNT CROSSES THE WIRE ─────────────────────────────────────────────\nDeliberately. The server re-reads the row in THIS org, re-prices it against\nthe invoice it actually built and clamps it (`priceCoupon`). A client that\ncould state its own reduction could make any job free.\n\n── DECLARING THEM IS NOT AUTHORIZING THEM ─────────────────────────────────\n`BookingIncentivesService.assertMayApplyIncentives` checks\n`invoices.discount:write` IN THE ORG BEING BILLED before any of this is\nhonoured, because a linked agent-customer arrives as PERSONAL_OWNER of\ntheir own org and passes every naive role check. They are declared here\nbecause `ValidationPipe` runs `forbidNonWhitelisted: true` — an undeclared\nfield is a 400 and therefore NO PROJECT AT ALL, which would turn a coupon\ntypo into a lost booking."
          },
          "discountCode": {
            "type": "string"
          },
          "rewardEntitlementId": {
            "type": "string",
            "description": "A `LoyaltyEntitlement` THE CUSTOMER ALREADY HOLDS, spent on this job.\n\nA reward is not a coupon and is never applied as one: it is a single-use\nasset the customer owns, it reduces the TAXABLE BASE (CRA taxes the\ndiscounted price), and it is claimed with a guarded UPDATE before any\ncredit is derived from it. See `orders/booking-incentives.ts`.\n\nOwnership is re-checked server-side against the booking's customer, so a\ncoordinator cannot spend one customer's reward on another's job."
          }
        },
        "required": [
          "scheduledTime"
        ]
      },
      "AssignProjectDto": {
        "type": "object",
        "properties": {
          "technicianId": {
            "type": "string"
          },
          "editorId": {
            "type": "string"
          },
          "override": {
            "type": "boolean",
            "description": "Confirm past an ASSIGNEE_ON_TIME_OFF 409 (authorized managers only)."
          }
        }
      },
      "UpdateListingStatusDto": {
        "type": "object",
        "properties": {
          "mlsLiveDate": {
            "type": "string",
            "nullable": true
          },
          "soldDate": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "UpdateProjectDto": {
        "type": "object",
        "properties": {
          "addressLine1": {
            "type": "string"
          },
          "addressLine2": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "notes": {
            "type": "string"
          },
          "clientDeliveryNote": {
            "type": "string",
            "description": "CLIENT-FACING note. Distinct from `notes`, which is internal. Kept as two\nfields on purpose: `notes` carries gate codes and internal coordination\nthat must never reach a client.",
            "maxLength": 2000
          },
          "scheduledTime": {
            "type": "string"
          },
          "packageId": {
            "type": "string",
            "nullable": true
          },
          "selectedAddOnIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "mediaTypes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "requiredCertLevelId": {
            "type": "string",
            "nullable": true,
            "description": "Required certification level for this project. Set this to gate\ndispatch eligibility — only techs holding a PASSED cert at this\nlevel (or higher in the same specialty's ladder) are eligible.\nPass null to clear."
          },
          "requiredKitId": {
            "type": "string",
            "nullable": true,
            "description": "Required equipment kit for this project. Used by the pre-shoot\nreadiness check + dispatcher eligibility filter. Pass null to clear."
          }
        }
      },
      "CancelProjectDto": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "maxLength": 1000
          },
          "path": {
            "type": "object",
            "enum": [
              "fee",
              "request"
            ]
          },
          "notifyCustomer": {
            "type": "boolean",
            "description": "When true, email the customer that their shoot was cancelled. Staff choose\nthis per-cancellation from the confirm dialog (some cancellations are\ninternal cleanup the client shouldn't hear about). Omitted → no email, so\nexisting callers (iOS, older webapp builds) keep today's silent behavior."
          }
        }
      },
      "BulkDeleteProjectsDto": {
        "type": "object",
        "properties": {
          "projectIds": {
            "minItems": 1,
            "maxItems": 1000,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "projectIds"
        ]
      },
      "BulkSetEditorsDto": {
        "type": "object",
        "properties": {
          "projectIds": {
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "addEditorIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "removeEditorIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "projectIds"
        ]
      },
      "UpdateProjectStatusDto": {
        "type": "object",
        "properties": {
          "status": {
            "type": "object"
          }
        },
        "required": [
          "status"
        ]
      },
      "EnableDeliveryDto": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "Per-send message. Included in the delivery email; not persisted.",
            "maxLength": 5000
          },
          "notifyEmail": {
            "type": "boolean"
          },
          "markAsDelivered": {
            "type": "boolean"
          },
          "sendInvoice": {
            "type": "boolean"
          },
          "additionalRecipients": {
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "mediaTypes": {
            "type": "array",
            "description": "The media types THIS send releases. Absent → release everything, i.e.\nexactly the behaviour every caller had before partial delivery existed.",
            "maxItems": 20,
            "items": {
              "type": "string",
              "enum": [
                "PHOTO",
                "VIDEO",
                "FLOORPLAN",
                "DOCUMENT",
                "VIRTUAL_TOUR",
                "PROPERTY_WEBSITE",
                "BROCHURE"
              ]
            }
          },
          "clientDeliveryNote": {
            "type": "string",
            "description": "The client-facing standing note, persisted to Project.clientDeliveryNote.\nSent from the dialog so it can be written while delivering.",
            "maxLength": 2000
          }
        }
      },
      "CreateVisitDto": {
        "type": "object",
        "properties": {
          "startTime": {
            "type": "string"
          },
          "contractedDurationMinutes": {
            "type": "number",
            "description": "The shoot's contracted length in minutes. When set, `endTime` is DERIVED\nfrom it (start + duration) so the calendar block and the pay run can never\ndescribe this shoot differently — the two used to be written by separate\ncode paths, and `contractedDurationMinutes` was never written at all.\n\nPreferred over `endTime` on this route: the webapp's shoot form leaves the\nend-time input optional, so extra shoots were routinely created with\nneither an end time nor a length.\n\nMUST stay declared: `whitelist: true` silently strips undeclared fields.",
            "minimum": 1,
            "maximum": 1440
          },
          "override": {
            "type": "boolean"
          },
          "endTime": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "technicianId": {
            "type": "string"
          },
          "technicianIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rescheduledFromVisitId": {
            "type": "string"
          }
        },
        "required": [
          "startTime"
        ]
      },
      "AddShootPricingDto": {
        "type": "object",
        "properties": {
          "mode": {
            "type": "string",
            "enum": [
              "add_items",
              "split"
            ]
          },
          "addOnIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "discountCents": {
            "type": "number",
            "minimum": 0
          },
          "couponCode": {
            "type": "string"
          }
        },
        "required": [
          "mode"
        ]
      },
      "AddShootDto": {
        "type": "object",
        "properties": {
          "startTime": {
            "type": "string"
          },
          "contractedDurationMinutes": {
            "type": "number",
            "description": "The added shoot's contracted length in minutes. When set, `endTime` is\nDERIVED from it so the calendar block and the pay run agree by construction.\n\nWhen neither this nor `endTime` is sent, an `add_items` shoot falls back to\nthe total duration of the add-ons it is being CHARGED for\n(`pricing.addOnIds`) — the length that was actually sold. A `split` shoot\nreuses the order's already-purchased items, so nothing implies a length for\nit and it is left with none rather than one being invented.\n\nMUST stay declared: `whitelist: true` silently strips undeclared fields.",
            "minimum": 1,
            "maximum": 1440
          },
          "endTime": {
            "type": "string"
          },
          "technicianIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "notes": {
            "type": "string"
          },
          "override": {
            "type": "boolean"
          },
          "pricing": {
            "$ref": "#/components/schemas/AddShootPricingDto"
          }
        },
        "required": [
          "startTime",
          "pricing"
        ]
      },
      "UpdateVisitDto": {
        "type": "object",
        "properties": {
          "startTime": {
            "type": "string"
          },
          "contractedDurationMinutes": {
            "type": "number",
            "nullable": true,
            "description": "THE MANUAL DURATION OVERRIDE, in minutes.\n\nA shoot's length is fixed at booking from the package it was sold with (plus\nits add-ons) and may thereafter change ONLY through an explicit gesture like\nthis one. Sending it rewrites `ProjectVisit.contractedDurationMinutes`, marks\nthe provenance MANUAL, records who changed it, and derives `endTime` from it\nso the calendar block and the pay run cannot describe the same shoot\ndifferently.\n\nWHY IT DID NOT EXIST BEFORE: `contractedDurationMinutes` shipped with a doc\ncomment promising \"a reviewer sets it from the pay run\", but no DTO carried\nit and no route wrote it — the column had ZERO writers in the entire\nproduct. The nearest thing was the optional end-time input, which rewrote a\nspan with no record that a human had decided anything. Note that\n`payrollOnSiteSecondsOverride` is NOT this field: that corrects the MEASURED\non-site time, this one sets the CONTRACT.\n\n`null` clears it back to \"not established\", which returns the shoot to being\nFLAGGED at pay time rather than paying a number nobody stands behind.\n\nTHAT SENTENCE IS TRUE ONLY BECAUSE THE WINDOW IS REMOVED WITH THE STAMP.\n`endTime` is DERIVED from this field on every write, so it took two rounds\nto make the promise real, and the second round is the one worth knowing:\n\n  • Clearing the stamp ALONE left the derived window in place, and\n    `resolveShootPaidSeconds` just dropped from precedence 2 to precedence 3\n    and re-paid the identical seconds as a `BOOKED_WINDOW` with an empty\n    attention array — the operator withdrew the number and nothing\n    downstream heard.\n  • Clearing it and REPLACING the window with a placeholder-length one was\n    worse on a 60-minute package: `packageCorroboratesVisitSpan` vouches for\n    a window that equals the package's own duration, which turns off the\n    very refusal the placeholder length was chosen to trigger, so\n    withdrawing a 45-minute stamp on such an order RAISED the shoot from\n    2700 s to 3600 s, still unflagged.\n\n`resolveVisitTimeWrite` now sets `endTime` to NULL when the window was the\ncleared stamp's own derivative. The shoot then states no length at all —\nnothing at any precedence, and no window for the package to vouch for — so\nit is `SHOOT_DURATION_MISSING` for every package duration, which is the\npromise above.\n\nSend `endTime` in the SAME request to mean \"clear the number but KEEP this\nwindow\" — an explicit window is the caller's own statement and is honoured\nas-is (still unstamped). That is what the shoot-duration dialog sends when\nit can land on the order's package.\n\nBounded to a day — a longer shoot is a data-entry slip, and an unbounded\nvalue here would be paid.\n\nMUST stay declared: `whitelist: true` silently strips undeclared body fields.",
            "minimum": 1,
            "maximum": 1440
          },
          "override": {
            "type": "boolean"
          },
          "endTime": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "missedReason": {
            "type": "object"
          },
          "notes": {
            "type": "string"
          },
          "technicianId": {
            "type": "string"
          },
          "technicianIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ApproveVisitDto": {
        "type": "object",
        "properties": {
          "approvalStatus": {
            "type": "object"
          },
          "rejectionReason": {
            "type": "string"
          }
        },
        "required": [
          "approvalStatus"
        ]
      },
      "SetVisitStartLocationDto": {
        "type": "object",
        "properties": {
          "addressLine1": {
            "type": "string",
            "maxLength": 200
          },
          "addressLine2": {
            "type": "string",
            "maxLength": 200
          },
          "city": {
            "type": "string",
            "maxLength": 120
          },
          "region": {
            "type": "string",
            "maxLength": 120
          },
          "postalCode": {
            "type": "string",
            "maxLength": 20
          },
          "countryCode": {
            "type": "string",
            "maxLength": 2
          },
          "lat": {
            "type": "number",
            "description": "Optional — the address is geocoded when these are absent."
          },
          "lng": {
            "type": "number"
          },
          "returnsHere": {
            "type": "boolean",
            "description": "Does the technician also RETURN here at the end of the day?\n\nDefaults false: a one-off start from somewhere else still ends at their\nreal home. True is the multi-day stay out of area. The end-of-day return\nleg is measured to whichever this resolves to, so a wrong default here\nsilently changes pay — which is why it is an explicit choice per shoot\nrather than a global rule."
          },
          "note": {
            "type": "string",
            "description": "Why. Shown to the technician on their own view of the shoot.",
            "maxLength": 500
          }
        }
      },
      "PresignUploadDto": {
        "type": "object",
        "properties": {
          "projectId": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "contentType": {
            "type": "string"
          },
          "mediaType": {
            "type": "string"
          }
        },
        "required": [
          "projectId",
          "filename",
          "contentType",
          "mediaType"
        ]
      },
      "PresignGeneralUploadDto": {
        "type": "object",
        "properties": {
          "filename": {
            "type": "string",
            "maxLength": 512
          },
          "contentType": {
            "type": "string",
            "maxLength": 255
          },
          "mediaType": {
            "type": "string",
            "maxLength": 64
          }
        },
        "required": [
          "filename",
          "contentType"
        ]
      },
      "ConfirmUploadDto": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "cdnUrl": {
            "type": "string"
          },
          "externalUrl": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "size": {
            "type": "number"
          },
          "type": {
            "type": "object"
          },
          "isRawUpload": {
            "type": "boolean"
          },
          "rawFolder": {
            "type": "string"
          },
          "projectId": {
            "type": "string"
          }
        },
        "required": [
          "filename",
          "size",
          "type",
          "projectId"
        ]
      },
      "SaveDriveConfigDto": {
        "type": "object",
        "properties": {
          "folder": {
            "type": "string",
            "description": "The parent folder, as either a Drive URL or a bare id. Admins paste the URL\nbecause that is what Drive's Share dialog hands them; the service accepts\nboth rather than making that a support ticket per org.",
            "maxLength": 500
          },
          "nameTemplate": {
            "type": "string",
            "description": "Tokens: {order} {address} {city} {client} {date}. Empty = platform default.",
            "maxLength": 200
          },
          "enabled": {
            "type": "boolean",
            "description": "Keep the configuration but stop creating folders."
          }
        }
      },
      "AvailabilityStatusDto": {
        "type": "object",
        "properties": {
          "isAvailable": {
            "type": "boolean"
          },
          "availabilityNote": {
            "type": "string",
            "nullable": true
          },
          "autoDeclineBookings": {
            "type": "boolean",
            "description": "REMOVED FEATURE — accepted and ignored. Nothing reads this any more.\n\nAuto-decline did two things, both deleted: it removed a technician from the\nPUBLIC booking pool entirely (every day, every slot — not just out-of-hours,\nwhich is what the name implied), silently hiding real availability from\ncustomers; and it turned an out-of-hours manual assignment into a hard 409.\nThe backing column is dropped in the accompanying migration.\n\nThe FIELD survives only because validation runs with\n`forbidNonWhitelisted: true`, so an unknown key 400s the WHOLE request. A\nbrowser tab opened before this shipped still PUTs `autoDeclineBookings`;\nremoving the field would break saving availability for those sessions.\nSafe to delete once stale clients have rolled over."
          }
        },
        "required": [
          "isAvailable"
        ]
      },
      "WorkHoursDto": {
        "type": "object",
        "properties": {
          "dayOfWeek": {
            "type": "object"
          },
          "isEnabled": {
            "type": "boolean"
          },
          "startTime": {
            "type": "string",
            "pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$"
          },
          "endTime": {
            "type": "string",
            "pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$"
          }
        },
        "required": [
          "dayOfWeek",
          "isEnabled",
          "startTime",
          "endTime"
        ]
      },
      "CreateWebhookSubscriptionDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        },
        "required": [
          "url",
          "events"
        ]
      },
      "UpdateWebhookSubscriptionDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri"
          },
          "events": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "isActive": {
            "type": "boolean"
          }
        }
      },
      "AttachmentDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "contentType": {
            "type": "string"
          },
          "size": {
            "type": "number"
          }
        },
        "required": [
          "url",
          "name",
          "contentType",
          "size"
        ]
      },
      "SendMessageDto": {
        "type": "object",
        "properties": {
          "projectId": {
            "type": "string"
          },
          "content": {
            "type": "string"
          },
          "channel": {
            "type": "object"
          },
          "thread": {
            "type": "string",
            "nullable": true
          },
          "mentionUserIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "attachments": {
            "maxItems": 10,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AttachmentDto"
            }
          }
        },
        "required": [
          "projectId"
        ]
      },
      "ReactMessageDto": {
        "type": "object",
        "properties": {
          "emoji": {
            "type": "string",
            "maxLength": 64
          }
        },
        "required": [
          "emoji"
        ]
      },
      "EditMessageDto": {
        "type": "object",
        "properties": {
          "content": {
            "type": "string"
          }
        },
        "required": [
          "content"
        ]
      },
      "EmailRouteDto": {
        "type": "object",
        "properties": {}
      },
      "PayInvoiceDto": {
        "type": "object",
        "properties": {
          "applyLoyaltyCredits": {
            "type": "boolean",
            "description": "Redeem the BILLED customer's loyalty credits against this invoice.\nHonoured only when the caller is the billed customer."
          },
          "loyaltyCreditsCents": {
            "type": "number",
            "description": "Requested redemption in cents. Always capped server-side against the live\napplicable balance — the client figure is never trusted.",
            "minimum": 0
          }
        }
      },
      "RefundInvoiceDto": {
        "type": "object",
        "properties": {
          "amountCents": {
            "type": "number",
            "description": "Optional partial amount in cents. Omit to refund the full remaining\nbalance on the underlying PaymentIntent. Must be > 0 and at most\n(amountReceived - alreadyRefunded) — Stripe rejects over-refunds.",
            "minimum": 1
          },
          "reason": {
            "type": "object",
            "description": "Stripe accepts three normalized reasons. Anything else gets passed\nalong as a metadata note (see `note`) but not as the Stripe reason."
          },
          "note": {
            "type": "string",
            "description": "Optional free-text note stored in Stripe metadata for audit. Helpful\nwhen the refund cause doesn't match one of the Stripe-defined reasons\n(e.g. \"agent rebooked\", \"weather cancellation\")."
          }
        }
      },
      "InvoiceLineItemDto": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string"
          },
          "descriptor": {
            "type": "string",
            "description": "Optional long-form descriptor — renders under the description on PDF / public invoice."
          },
          "quantity": {
            "type": "number"
          },
          "unitPrice": {
            "type": "number"
          },
          "total": {
            "type": "number"
          },
          "taxTreatment": {
            "type": "object",
            "enum": [
              "TAXABLE",
              "EXEMPT"
            ]
          }
        },
        "required": [
          "description",
          "quantity",
          "unitPrice"
        ]
      },
      "CreateInvoiceDto": {
        "type": "object",
        "properties": {
          "customerId": {
            "type": "string"
          },
          "projectId": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceLineItemDto"
            }
          },
          "taxRate": {
            "type": "number"
          },
          "dueDate": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          }
        },
        "required": [
          "items"
        ]
      },
      "UpdateInvoiceDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceLineItemDto"
            }
          },
          "taxRate": {
            "type": "number"
          },
          "dueDate": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "projectId": {
            "type": "string",
            "nullable": true
          },
          "customerId": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ResyncInvoiceDto": {
        "type": "object",
        "properties": {
          "expectedTotalCents": {
            "type": "number",
            "description": "`InvoiceResyncPreview.expectedTotalCents` from the preview the operator\nconfirmed. Tax-inclusive, after any flat discount, in the invoice's own\ncurrency — i.e. the number on the confirm button."
          }
        }
      },
      "ConfirmScopeChangeDto": {
        "type": "object",
        "properties": {
          "expectedTotalCents": {
            "type": "number",
            "description": "`ProjectInvoiceDrift.expectedTotalCents` from the panel the operator\nconfirmed: the tax-inclusive total the invoice will be rewritten TO, after\nany flat discount, in the invoice's own currency."
          },
          "resend": {
            "type": "boolean",
            "description": "Re-deliver the document after updating it.\n\nDefaults to TRUE when omitted — see the controller. A customer holding a\nPDF for the old figure has to be told, and silently leaving them on a stale\nnumber is the failure this whole feature exists to remove. Sending is\nbest-effort: a delivery failure never undoes the update."
          }
        },
        "required": [
          "expectedTotalCents"
        ]
      },
      "CreateSupplementaryInvoiceDto": {
        "type": "object",
        "properties": {
          "expectedDifferenceCents": {
            "type": "number",
            "description": "`ProjectInvoiceDrift.supplementary.totalCents` — the tax-inclusive amount\nof the NEW document, in the settled invoice's own currency, net of every\nsupplementary already raised against it.\n\nRequired, and compared exactly: this call creates a bill."
          }
        },
        "required": [
          "expectedDifferenceCents"
        ]
      },
      "MarkInvoicePaidDto": {
        "type": "object",
        "properties": {
          "paymentMethod": {
            "type": "string",
            "enum": [
              "STRIPE",
              "CASH",
              "CHECK",
              "ETRANSFER",
              "BANK_TRANSFER",
              "OTHER"
            ],
            "description": "How the off-platform payment was collected. STRIPE is rejected — card payments are recorded automatically by the Stripe webhook.",
            "example": "ETRANSFER"
          },
          "paidAt": {
            "type": "string",
            "description": "When the payment was received (ISO 8601). Defaults to now if omitted."
          }
        },
        "required": [
          "paymentMethod"
        ]
      },
      "ApplyDiscountDto": {
        "type": "object",
        "properties": {
          "discountId": {
            "type": "string",
            "description": "OrgDiscount id (named discount or coupon)"
          },
          "code": {
            "type": "string",
            "maxLength": 64,
            "description": "Coupon code (case-insensitive)"
          }
        }
      },
      "PortalSessionDto": {
        "type": "object",
        "properties": {}
      },
      "RedeemReferralDto": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string",
            "maxLength": 64
          }
        },
        "required": [
          "code"
        ]
      },
      "SetReferralTokenDto": {
        "type": "object",
        "properties": {
          "orgId": {
            "type": "string"
          },
          "token": {
            "type": "string",
            "minLength": 3,
            "maxLength": 32,
            "pattern": "^[a-zA-Z0-9._-]+$"
          }
        },
        "required": [
          "orgId",
          "token"
        ]
      },
      "AttributeManualReferralDto": {
        "type": "object",
        "properties": {
          "referrerCustomerId": {
            "type": "string",
            "description": "The customer who did the referring — must be a customer of THIS org."
          },
          "refereeCustomerId": {
            "type": "string",
            "description": "The customer who was referred — must be a customer of THIS org."
          },
          "reason": {
            "type": "string",
            "description": "WHY this is being recorded. Required, because the whole point of the\nMANUAL source is that a human vouched for something the platform did not\nsee — a row that says \"manual\" with no reason is an assertion with no\nauthor's account of it.\n\n`@MaxLength(500)` matches ATTRIBUTION_REASON_MAX in the web client\n(apps/frontend/lib/referral-attribution.ts), which is itself the same limit\nthe loyalty manual-adjustment reason uses — one org-wide dialect for \"why\ndid a human do this by hand\". A client that counts to a different number\nwould let a coordinator type a reason the server then rejects.\n\nTrimmed before validation so a whitespace-only string fails @MinLength\nrather than being stored as a blank reason.",
            "minLength": 1,
            "maxLength": 500
          }
        },
        "required": [
          "referrerCustomerId",
          "refereeCustomerId",
          "reason"
        ]
      },
      "MemberShiftDayDto": {
        "type": "object",
        "properties": {
          "dayOfWeek": {
            "type": "object"
          },
          "isEnabled": {
            "type": "boolean"
          },
          "startTime": {
            "type": "string",
            "pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$"
          },
          "endTime": {
            "type": "string",
            "pattern": "^([01]?[0-9]|2[0-3]):[0-5][0-9]$"
          }
        },
        "required": [
          "dayOfWeek",
          "isEnabled",
          "startTime",
          "endTime"
        ]
      },
      "UpdateMemberShiftsDto": {
        "type": "object",
        "properties": {
          "shifts": {
            "minItems": 1,
            "maxItems": 7,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MemberShiftDayDto"
            }
          }
        },
        "required": [
          "shifts"
        ]
      },
      "SubmitVideoScriptFeedbackDto": {
        "type": "object",
        "properties": {
          "body": {
            "type": "string",
            "description": "The note. \"The second one is too salesy\", \"it's a townhouse, not a condo\",\n\"the seller called and wants the garage mentioned\".\n\nREQUIRED, unlike `GenerateVideoScriptsDto.feedback`, and that difference is\nthe whole point of the route: generating without notes is a legitimate\nthing for the company to do, whereas submitting an EMPTY note is a row that\nsays nothing, notifies the company for nothing, and then feeds a blank line\ninto the model's prompt. `MinLength(1)` runs after class-validator has the\nraw string, so a body of only whitespace still gets through here — the\nservice trims and refuses it, which is the one check that has to live where\nevery present and future caller passes.\n\nThe 2000-character cap matches the generate route's, and for the same\nreason: this text goes STRAIGHT INTO AN LLM PROMPT, and the cap is the only\nthing between a paste-bomb and the token bill. Notes ACCUMULATE now — a\ngeneration composes every unconsumed one — so the cap is per note and the\nservice caps the composed total as well.",
            "minLength": 1,
            "maxLength": 2000
          }
        },
        "required": [
          "body"
        ]
      },
      "GenerateVideoScriptsDto": {
        "type": "object",
        "properties": {
          "feedback": {
            "type": "string",
            "description": "The agent's written notes on the previous set — \"the second one is too\nsalesy\", \"it's a townhouse, not a condo\", \"mention the ravine\".\n\nAbsent on the first generation: nobody asked for it, the order did.",
            "maxLength": 2000
          },
          "lengthSeconds": {
            "type": "number",
            "description": "How long the finished video runs, in seconds. 30 is the shortest cut worth\nmaking and 120 the longest anyone watches; 60 is the ordinary floor and the\nbuilt-in default.\n\n`@IsInt` as well as `@IsIn` so `60.5` is rejected as the wrong TYPE rather\nthan falling through the membership check with a confusing message.",
            "enum": [
              30,
              60,
              90,
              120
            ]
          },
          "tone": {
            "type": "object",
            "description": "professional | warm | energetic | luxury. The register the agent speaks in.",
            "enum": [
              "professional",
              "warm",
              "energetic",
              "luxury"
            ]
          },
          "pace": {
            "type": "object",
            "description": "calm | measured | fast. Sentence RHYTHM — how many sentences the word\nbudget is spent across, never how many words there are.",
            "enum": [
              "calm",
              "measured",
              "fast"
            ]
          },
          "leadWith": {
            "type": "object",
            "description": "lifestyle | features | location. What the hook opens on.",
            "enum": [
              "lifestyle",
              "features",
              "location"
            ]
          },
          "boldness": {
            "type": "object",
            "description": "safe | balanced | bold. How far the WRITING may depart from a literal\ndescription.\n\nIT DOES NOT TOUCH SAMPLING TEMPERATURE, and must never be made to — see the\ndocblock on `SCRIPT_BOLDNESS`. It changes instruction language and\nstructural latitude; grounding is absolute at every setting.",
            "enum": [
              "safe",
              "balanced",
              "bold"
            ]
          }
        }
      },
      "CreateBookingFormDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "isDefault": {
            "type": "boolean"
          },
          "showOnPublic": {
            "type": "boolean"
          },
          "showOnPortal": {
            "type": "boolean"
          },
          "questions": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "config": {
            "type": "object"
          },
          "displayOrder": {
            "type": "number"
          },
          "promotionId": {
            "type": "string",
            "nullable": true
          },
          "promoEnabled": {
            "type": "object"
          },
          "promoKind": {
            "type": "object"
          },
          "promoPayload": {
            "type": "object"
          },
          "promoDisplayName": {
            "type": "object"
          },
          "promoDescription": {
            "type": "object"
          },
          "promoNewCustomersOnly": {
            "type": "object"
          },
          "promoStartsAt": {
            "type": "object"
          },
          "promoEndsAt": {
            "type": "object"
          },
          "promoMaxClaims": {
            "type": "object"
          }
        },
        "required": [
          "name"
        ]
      },
      "UpdateBookingFormDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "isDefault": {
            "type": "boolean"
          },
          "showOnPublic": {
            "type": "boolean"
          },
          "showOnPortal": {
            "type": "boolean"
          },
          "questions": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "config": {
            "type": "object"
          },
          "displayOrder": {
            "type": "number"
          },
          "promotionId": {
            "type": "string",
            "nullable": true
          },
          "promoEnabled": {
            "type": "object"
          },
          "promoKind": {
            "type": "object"
          },
          "promoPayload": {
            "type": "object"
          },
          "promoDisplayName": {
            "type": "object"
          },
          "promoDescription": {
            "type": "object"
          },
          "promoNewCustomersOnly": {
            "type": "object"
          },
          "promoStartsAt": {
            "type": "object"
          },
          "promoEndsAt": {
            "type": "object"
          },
          "promoMaxClaims": {
            "type": "object"
          }
        }
      },
      "ApproveCancellationRequestDto": {
        "type": "object",
        "properties": {
          "chargeFee": {
            "type": "boolean"
          },
          "chargedAmountCents": {
            "type": "number",
            "minimum": 0
          },
          "currency": {
            "type": "string"
          },
          "note": {
            "type": "string"
          }
        }
      },
      "DenyCancellationRequestDto": {
        "type": "object",
        "properties": {
          "note": {
            "type": "string",
            "maxLength": 500
          }
        }
      },
      "CreatePropertyDto": {
        "type": "object",
        "properties": {
          "projectId": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "standard": {
            "type": "object"
          }
        }
      },
      "UpdatePropertyDto": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string"
          },
          "standard": {
            "type": "object"
          },
          "measurementBasis": {
            "type": "object"
          },
          "status": {
            "type": "object"
          }
        }
      },
      "CreateFloorDto": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string"
          },
          "level": {
            "type": "number"
          },
          "isAboveGrade": {
            "type": "boolean"
          },
          "wallThicknessInches": {
            "type": "number",
            "minimum": 2,
            "maximum": 24
          }
        },
        "required": [
          "label",
          "level"
        ]
      },
      "UpdateFloorDto": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string"
          },
          "level": {
            "type": "number"
          },
          "isAboveGrade": {
            "type": "boolean"
          },
          "countsTowardGla": {
            "type": "boolean",
            "description": "Does this floor count toward Gross Living Area? Default true. Set false\n for unfinished space (e.g. an unfinished basement) to exclude it from GLA\n while it still counts in total/exterior area. Writing it re-runs the\n property area recompute so totals refresh immediately."
          },
          "wallThicknessInches": {
            "type": "number",
            "minimum": 2,
            "maximum": 24
          },
          "interiorWallThicknessInches": {
            "type": "number",
            "description": "Interior partition thickness. Stored inside Floor.layoutJson (not a column) to avoid a\nPrisma migration; the service merges this into layoutJson when present.",
            "minimum": 1,
            "maximum": 20
          },
          "hidden": {
            "type": "boolean",
            "description": "Hide / un-hide the floor — see `Floor.hiddenAt`. Stored as a timestamp,\nsent as a boolean (matching `TourNode.hidden`, the same concept one grain\ndown).\n\nMUST stay declared here even though the service routes it to\n`setFloorHidden` (which carries a last-visible-floor guard) rather than\nwriting it as a plain column: with `whitelist: true`, an UNDECLARED field is\nsilently stripped from the body, so a PATCH carrying `{ hidden: true }`\nwould answer 200 having changed nothing. The dedicated\n`PATCH .../floors/:floorId/hidden` route is what the web UI uses; this keeps\nevery other client out of that trap."
          },
          "layoutJson": {
            "type": "object"
          }
        }
      },
      "SetFloorHiddenDto": {
        "type": "object",
        "properties": {
          "hidden": {
            "type": "boolean"
          }
        },
        "required": [
          "hidden"
        ]
      },
      "CreateRoomDto": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string"
          },
          "roomType": {
            "type": "object"
          },
          "ceilingHeight": {
            "type": "number"
          },
          "isIncludedInGla": {
            "type": "boolean"
          },
          "displayOrder": {
            "type": "number"
          },
          "notes": {
            "type": "string"
          }
        },
        "required": [
          "label"
        ]
      },
      "UpdateRoomDto": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string"
          },
          "roomType": {
            "type": "object"
          },
          "ceilingHeight": {
            "type": "number"
          },
          "isIncludedInGla": {
            "type": "boolean"
          },
          "excludeReason": {
            "type": "string",
            "description": "User-supplied exclusion reason. Stored as free text but the picker\nsurfaces SUNROOM / UNFINISHED / ATTIC / LOW_CEILING / OTHER. Pass an\nempty string to clear (back to auto-derivation)."
          },
          "displayOrder": {
            "type": "number"
          },
          "notes": {
            "type": "string"
          },
          "mirrorX": {
            "type": "boolean"
          }
        }
      },
      "DoorInput": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "doorType": {
            "type": "object"
          },
          "widthInches": {
            "type": "number"
          },
          "offsetInches": {
            "type": "number"
          },
          "connectsToRoomId": {
            "type": "string"
          }
        }
      },
      "WindowInput": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "windowType": {
            "type": "object"
          },
          "widthInches": {
            "type": "number"
          },
          "heightInches": {
            "type": "number"
          },
          "offsetInches": {
            "type": "number"
          },
          "depthInches": {
            "type": "number",
            "description": "Phase 6: BAY/BOW perpendicular protrusion from the wall (in)."
          },
          "outerWidthInches": {
            "type": "number",
            "description": "Phase 6: BAY trapezoid outer-wall width (in). NULL for BOW."
          }
        }
      },
      "WallInput": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "wallIndex": {
            "type": "number"
          },
          "lengthInches": {
            "type": "number"
          },
          "angleDegrees": {
            "type": "number"
          },
          "azimuthDegrees": {
            "type": "number"
          },
          "adjacentRoomId": {
            "type": "string"
          },
          "adjacentWallIndex": {
            "type": "number"
          },
          "doors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DoorInput"
            }
          },
          "windows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WindowInput"
            }
          }
        },
        "required": [
          "wallIndex",
          "lengthInches"
        ]
      },
      "SaveMeasurementsDto": {
        "type": "object",
        "properties": {
          "walls": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WallInput"
            }
          }
        },
        "required": [
          "walls"
        ]
      },
      "WalkShot": {
        "type": "object",
        "properties": {
          "distance": {
            "type": "number"
          },
          "azimuth": {
            "type": "number"
          },
          "inclination": {
            "type": "number"
          },
          "roll": {
            "type": "number"
          },
          "sequence": {
            "type": "number"
          }
        },
        "required": [
          "distance",
          "azimuth",
          "inclination",
          "roll",
          "sequence"
        ]
      },
      "WalkDoorInput": {
        "type": "object",
        "properties": {
          "wallIndex": {
            "type": "number"
          },
          "doorType": {
            "type": "object"
          },
          "widthInches": {
            "type": "number"
          },
          "offsetInches": {
            "type": "number"
          }
        },
        "required": [
          "wallIndex"
        ]
      },
      "WalkWindowInput": {
        "type": "object",
        "properties": {
          "wallIndex": {
            "type": "number"
          },
          "windowType": {
            "type": "object"
          },
          "widthInches": {
            "type": "number"
          },
          "heightInches": {
            "type": "number"
          },
          "offsetInches": {
            "type": "number"
          },
          "depthInches": {
            "type": "number",
            "description": "Phase 6: BAY/BOW perpendicular protrusion from the wall (in)."
          },
          "outerWidthInches": {
            "type": "number",
            "description": "Phase 6: BAY trapezoid outer-wall width (in). NULL for BOW."
          }
        },
        "required": [
          "wallIndex"
        ]
      },
      "SaveWalkDto": {
        "type": "object",
        "properties": {
          "shots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WalkShot"
            }
          },
          "label": {
            "type": "string"
          },
          "roomType": {
            "type": "string"
          },
          "ceilingHeight": {
            "type": "number"
          },
          "doors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WalkDoorInput"
            }
          },
          "windows": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WalkWindowInput"
            }
          }
        },
        "required": [
          "shots"
        ]
      },
      "CreatePackageDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 160,
            "description": "Package name"
          },
          "description": {
            "type": "string",
            "description": "Package description"
          },
          "price": {
            "type": "number",
            "minimum": 0,
            "description": "Price in cents"
          },
          "currency": {
            "type": "string",
            "maxLength": 12,
            "description": "Currency code (default: usd)"
          },
          "priceByAreaCents": {
            "type": "object",
            "description": "Per-service-area price overrides in cents, keyed by service-area id ({ [serviceAreaId]: cents }). Each area uses its own currency; areas not listed fall back to `price`."
          },
          "cogsPerOrderCentsByMediaType": {
            "type": "object",
            "description": "COST OF GOODS SOLD (optional) — the OUTSOURCED production cost of this package’s included items, in integer cents, keyed by MediaType ({ \"PHOTO\": 4500, \"VIDEO\": 12000 }). PER ORDER, not per unit: nothing in the schema persists a per-item quantity, so a figure here is the whole outsourcing cost for that media type on ONE project. An ABSENT key means nobody has set a figure — the Cost & Profit screen reports that as unknown and never counts it as zero; an explicit 0 is a DELIBERATE zero (produced in-house) and is a known figure. Send null for a key to clear it back to unknown. Keys must be MediaType values and values whole cents >= 0; anything else is rejected rather than silently dropped, because a silently-dropped cost is indistinguishable from \"not set\". Denominated in the package currency. Excludes technician labour, which is computed separately from real pay rates."
          },
          "mediaTypes": {
            "type": "array",
            "description": "Included media types",
            "items": {
              "type": "string",
              "enum": [
                "PHOTO",
                "VIDEO",
                "FLOORPLAN",
                "DOCUMENT",
                "VIRTUAL_TOUR",
                "PROPERTY_WEBSITE",
                "BROCHURE"
              ]
            }
          },
          "turnaroundDays": {
            "type": "number",
            "minimum": 0,
            "description": "Turnaround time in days"
          },
          "estimatedDurationMinutes": {
            "type": "number",
            "minimum": 0,
            "description": "Estimated on-site duration in minutes"
          },
          "photoCount": {
            "type": "number",
            "minimum": 0,
            "description": "Number of photos included"
          },
          "videoMinutes": {
            "type": "number",
            "minimum": 0,
            "description": "Minutes of video included"
          },
          "features": {
            "description": "Feature list",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "images": {
            "description": "Image URLs",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "mediaAssets": {
            "description": "Rich media assets (images, videos, 3D, comparison)",
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "variants": {
            "description": "Customer-selectable product variants",
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "includedTechnicianIds": {
            "description": "Technician user IDs that are explicitly allowed for this package",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "excludedTechnicianIds": {
            "description": "Technician user IDs that are excluded from this package",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "serviceAreaIds": {
            "description": "Service area IDs this package is available in (empty = all areas)",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "displayOrder": {
            "type": "number",
            "minimum": 0,
            "description": "Display order"
          },
          "questions": {
            "description": "Package-specific intake questions (appended to the booking form questions when this package is selected). BookingQuestion[].",
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "questionSetIds": {
            "description": "IDs of BookingQuestionSet rows to apply when this package is selected. Lets the same set be reused across many packages.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "includedAddOnIds": {
            "description": "Money Model v3 — explicit IDs of PackageAddOn rows that this pre-built package \"includes\" for bundle-detection. When the Build-Your-Own picker selection is a superset of this list, the booking flow surfaces a \"switch to bundle and save $X\" callout. See docs/money-model-v3-design.md.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "recommendedAddOnIds": {
            "description": "Add-on IDs the company chose to upsell for this package — surfaced as \"Recommended\" (badge + top-sort) in the booking flow add-on step.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "requiredKitId": {
            "type": "string",
            "nullable": true,
            "description": "P2 — id of the gear Kit this package requires on site. Seeded onto Project.requiredKitId when a booking selects this package, so the assignment UI can surface tech-readiness. Null/omitted = no specific kit required."
          },
          "requiredCertLevelId": {
            "type": "string",
            "nullable": true,
            "description": "id of the CertificationLevel a technician must hold (PASSED, non-expired) to serve this package. Null/omitted = no certification required."
          }
        },
        "required": [
          "name",
          "price",
          "mediaTypes"
        ]
      },
      "UpdatePackageDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 160,
            "description": "Package name"
          },
          "description": {
            "type": "string",
            "description": "Package description"
          },
          "price": {
            "type": "number",
            "minimum": 0,
            "description": "Price in cents"
          },
          "currency": {
            "type": "string",
            "maxLength": 12,
            "description": "Currency code"
          },
          "priceByAreaCents": {
            "type": "object",
            "description": "Per-service-area price overrides in cents, keyed by service-area id ({ [serviceAreaId]: cents }). Each area uses its own currency; areas not listed fall back to `price`."
          },
          "cogsPerOrderCentsByMediaType": {
            "type": "object",
            "description": "COST OF GOODS SOLD (optional) — outsourced production cost per included media type, in integer cents, keyed by MediaType ({ \"PHOTO\": 4500, \"VIDEO\": 12000 }). PER ORDER, not per unit. REPLACES the stored map wholesale, so send every entry you want kept; omit the field entirely to leave it unchanged, send {} to clear every figure, or send null for a single key to clear just that one. An absent key = nobody set a figure (reported as unknown, never zero); an explicit 0 = a deliberate zero (produced in-house) and is a known figure. Keys must be MediaType values and values whole cents >= 0; anything else is rejected rather than silently dropped. Denominated in the package currency. Excludes technician labour."
          },
          "mediaTypes": {
            "type": "array",
            "description": "Included media types",
            "items": {
              "type": "string",
              "enum": [
                "PHOTO",
                "VIDEO",
                "FLOORPLAN",
                "DOCUMENT",
                "VIRTUAL_TOUR",
                "PROPERTY_WEBSITE",
                "BROCHURE"
              ]
            }
          },
          "isActive": {
            "type": "boolean",
            "description": "Whether package is active"
          },
          "turnaroundDays": {
            "type": "number",
            "minimum": 0,
            "description": "Turnaround time in days"
          },
          "estimatedDurationMinutes": {
            "type": "number",
            "minimum": 0,
            "description": "Estimated on-site duration in minutes"
          },
          "photoCount": {
            "type": "number",
            "minimum": 0,
            "description": "Number of photos included"
          },
          "videoMinutes": {
            "type": "number",
            "minimum": 0,
            "description": "Minutes of video included"
          },
          "features": {
            "description": "Feature list",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "images": {
            "description": "Image URLs",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "mediaAssets": {
            "description": "Rich media assets (images, videos, 3D, comparison)",
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "variants": {
            "description": "Customer-selectable product variants",
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "includedTechnicianIds": {
            "description": "Technician user IDs that are explicitly allowed for this package",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "excludedTechnicianIds": {
            "description": "Technician user IDs that are excluded from this package",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "serviceAreaIds": {
            "description": "Service area IDs this package is available in (empty = all areas)",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "displayOrder": {
            "type": "number",
            "minimum": 0,
            "description": "Display order"
          },
          "questions": {
            "description": "Package-specific intake questions (appended to the booking form questions when this package is selected). BookingQuestion[]. Pass empty array to clear.",
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "questionSetIds": {
            "description": "IDs of BookingQuestionSet rows to apply when this package is selected. Pass empty array to clear.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "includedAddOnIds": {
            "description": "Money Model v3 — explicit IDs of PackageAddOn rows that this pre-built package \"includes\" for bundle-detection. When the Build-Your-Own picker selection is a superset of this list, the booking flow surfaces a \"switch to bundle and save $X\" callout. Pass empty array to clear. See docs/money-model-v3-design.md.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "recommendedAddOnIds": {
            "description": "Add-on IDs the company chose to upsell for this package — surfaced as \"Recommended\" (badge + top-sort) in the booking flow add-on step.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "requiredKitId": {
            "type": "string",
            "nullable": true,
            "description": "P2 — id of the gear Kit this package requires on site. Pass null to clear. Seeded onto Project.requiredKitId when a booking selects this package."
          },
          "requiredCertLevelId": {
            "type": "string",
            "nullable": true,
            "description": "id of the CertificationLevel a technician must hold (PASSED, non-expired) to serve this package. Pass null to clear. Null/omitted = no certification required."
          }
        }
      },
      "CreateAddOnDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 160,
            "description": "Add-on name"
          },
          "description": {
            "type": "string",
            "description": "Add-on description"
          },
          "price": {
            "type": "number",
            "minimum": 0,
            "description": "Price in cents"
          },
          "currency": {
            "type": "string",
            "maxLength": 12,
            "description": "Currency code (default: usd)"
          },
          "priceByAreaCents": {
            "type": "object",
            "description": "Per-service-area price overrides in cents, keyed by service-area id ({ [serviceAreaId]: cents }). Each area uses its own currency; areas not listed fall back to `price`."
          },
          "cogsPerOrderCents": {
            "type": "number",
            "nullable": true,
            "minimum": 0,
            "description": "COST OF GOODS SOLD (optional) — the OUTSOURCED production cost of this add-on, in integer cents, PER ORDER (not per unit: nothing persists a per-item quantity, so this is the whole outsourcing cost for this add-on on ONE project). Omit to leave unchanged; send null to state that no outsourcing cost is known, which CLEARS any existing figure. null and 0 are different facts and are treated differently downstream: null = nobody has set it, which the Cost & Profit screen reports as unknown and refuses to count as zero; 0 = a deliberate zero (produced in-house), a known figure that does not flag. Denominated in the add-on currency. Excludes technician labour, which is computed separately from real pay rates."
          },
          "category": {
            "type": "string",
            "description": "Add-on category",
            "enum": [
              "AERIAL",
              "TWILIGHT",
              "VIRTUAL_TOUR",
              "FLOORPLAN",
              "RUSH",
              "OTHER",
              "PROPERTY_WEBSITE"
            ]
          },
          "mediaTypes": {
            "type": "array",
            "description": "Included media types",
            "items": {
              "type": "string",
              "enum": [
                "PHOTO",
                "VIDEO",
                "FLOORPLAN",
                "DOCUMENT",
                "VIRTUAL_TOUR",
                "PROPERTY_WEBSITE",
                "BROCHURE"
              ]
            }
          },
          "features": {
            "description": "Feature list",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "images": {
            "description": "Image URLs",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "mediaAssets": {
            "description": "Rich media assets (images, videos, 3D, comparison)",
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "variants": {
            "description": "Customer-selectable product variants",
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "estimatedDurationMinutes": {
            "type": "number",
            "minimum": 0,
            "description": "Estimated on-site duration in minutes"
          },
          "photoCount": {
            "type": "number",
            "minimum": 0,
            "description": "Number of photos included"
          },
          "videoMinutes": {
            "type": "number",
            "minimum": 0,
            "description": "Minutes of video included"
          },
          "serviceAreaIds": {
            "description": "Service area IDs this add-on is available in (empty = all areas)",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "displayOrder": {
            "type": "number",
            "minimum": 0,
            "description": "Display order"
          },
          "bundledWithPackageIds": {
            "description": "Package IDs this add-on is bundled with. When the agent selects one of these packages, this add-on shows a \"Bundle deal\" badge and uses bundledPriceCents.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "bundledPriceCents": {
            "type": "number",
            "minimum": 0,
            "description": "Discounted price (in cents) when this add-on is bundled with a matching package. Falls back to `price` when omitted."
          },
          "excludedPackageIds": {
            "description": "Package IDs where this add-on is redundant. When one of these packages is selected, the add-on is hidden from the booking flow and refused at checkout.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "standalonePriceCents": {
            "type": "number",
            "minimum": 0,
            "description": "Money Model v3 — optional standalone price (cents) when this add-on is picked à la carte. The Build-Your-Own pricing engine applies a +20% premium on top of this value. Falls back to `price` when null. See docs/money-model-v3-design.md."
          },
          "isPickerItem": {
            "type": "boolean",
            "description": "Money Model v3 — when true, this add-on renders as a selectable item in the booking-flow Build-Your-Own picker. The per-org buildYourOwnEnabled flag gates the picker entirely; this controls which add-ons appear inside it. See docs/money-model-v3-design.md."
          },
          "notOrderableAlone": {
            "type": "boolean",
            "description": "#10a — when true, this add-on cannot be ordered alone in the Build-Your-Own picker; the customer must also select a companion item (requiresAddOnIds) or a package that includes it. Enforced in the booking flow and refused at order time."
          },
          "requiresAddOnIds": {
            "description": "#10a — co-requisite add-on ids that MUST also be selected before this item can be added to a Build-Your-Own cart. Empty = no companions required.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "requiredKitId": {
            "type": "string",
            "nullable": true,
            "description": "P2 — id of the gear Kit this add-on requires on site. Falls back onto Project.requiredKitId when the booking has no package (à-la-carte) or the package carries no required kit. Null/omitted = no specific kit."
          },
          "requiredCertLevelId": {
            "type": "string",
            "nullable": true,
            "description": "id of the CertificationLevel a technician must hold (PASSED, non-expired) to serve this add-on. Null/omitted = no certification required."
          }
        },
        "required": [
          "name",
          "price"
        ]
      },
      "UpdateAddOnDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 160,
            "description": "Add-on name"
          },
          "description": {
            "type": "string",
            "description": "Add-on description"
          },
          "price": {
            "type": "number",
            "minimum": 0,
            "description": "Price in cents"
          },
          "currency": {
            "type": "string",
            "maxLength": 12,
            "description": "Currency code"
          },
          "priceByAreaCents": {
            "type": "object",
            "description": "Per-service-area price overrides in cents, keyed by service-area id ({ [serviceAreaId]: cents }). Each area uses its own currency; areas not listed fall back to `price`."
          },
          "cogsPerOrderCents": {
            "type": "number",
            "nullable": true,
            "minimum": 0,
            "description": "COST OF GOODS SOLD (optional) — the OUTSOURCED production cost of this add-on, in integer cents, PER ORDER (not per unit: nothing persists a per-item quantity, so this is the whole outsourcing cost for this add-on on ONE project). Omit to leave unchanged; send null to state that no outsourcing cost is known, which CLEARS any existing figure. null and 0 are different facts and are treated differently downstream: null = nobody has set it, which the Cost & Profit screen reports as unknown and refuses to count as zero; 0 = a deliberate zero (produced in-house), a known figure that does not flag. Denominated in the add-on currency. Excludes technician labour, which is computed separately from real pay rates."
          },
          "category": {
            "type": "string",
            "description": "Add-on category",
            "enum": [
              "AERIAL",
              "TWILIGHT",
              "VIRTUAL_TOUR",
              "FLOORPLAN",
              "RUSH",
              "OTHER",
              "PROPERTY_WEBSITE"
            ]
          },
          "mediaTypes": {
            "type": "array",
            "description": "Included media types",
            "items": {
              "type": "string",
              "enum": [
                "PHOTO",
                "VIDEO",
                "FLOORPLAN",
                "DOCUMENT",
                "VIRTUAL_TOUR",
                "PROPERTY_WEBSITE",
                "BROCHURE"
              ]
            }
          },
          "features": {
            "description": "Feature list",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "images": {
            "description": "Image URLs",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "mediaAssets": {
            "description": "Rich media assets (images, videos, 3D, comparison)",
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "variants": {
            "description": "Customer-selectable product variants",
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "estimatedDurationMinutes": {
            "type": "number",
            "minimum": 0,
            "description": "Estimated on-site duration in minutes"
          },
          "photoCount": {
            "type": "number",
            "minimum": 0,
            "description": "Number of photos included"
          },
          "videoMinutes": {
            "type": "number",
            "minimum": 0,
            "description": "Minutes of video included"
          },
          "serviceAreaIds": {
            "description": "Service area IDs this add-on is available in (empty = all areas)",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "isActive": {
            "type": "boolean",
            "description": "Whether add-on is active"
          },
          "displayOrder": {
            "type": "number",
            "minimum": 0,
            "description": "Display order"
          },
          "bundledWithPackageIds": {
            "description": "Package IDs this add-on is bundled with. When the agent selects one of these packages, this add-on shows a \"Bundle deal\" badge and uses bundledPriceCents.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "bundledPriceCents": {
            "type": "number",
            "minimum": 0,
            "description": "Discounted price (in cents) when this add-on is bundled with a matching package. Falls back to `price` when omitted."
          },
          "excludedPackageIds": {
            "description": "Package IDs where this add-on is redundant. When one of these packages is selected, the add-on is hidden from the booking flow and refused at checkout.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "standalonePriceCents": {
            "type": "number",
            "minimum": 0,
            "description": "Money Model v3 — optional standalone price (cents) when this add-on is picked à la carte. The Build-Your-Own pricing engine applies a +20% premium on top of this value. Falls back to `price` when null. See docs/money-model-v3-design.md."
          },
          "isPickerItem": {
            "type": "boolean",
            "description": "Money Model v3 — when true, this add-on renders as a selectable item in the booking-flow Build-Your-Own picker. The per-org buildYourOwnEnabled flag gates the picker entirely; this controls which add-ons appear inside it. See docs/money-model-v3-design.md."
          },
          "notOrderableAlone": {
            "type": "boolean",
            "description": "#10a — when true, this add-on cannot be ordered alone in the Build-Your-Own picker; the customer must also select a companion item (requiresAddOnIds) or a package that includes it. Enforced in the booking flow and refused at order time."
          },
          "requiresAddOnIds": {
            "description": "#10a — co-requisite add-on ids that MUST also be selected before this item can be added to a Build-Your-Own cart. Pass empty array to clear.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "requiredKitId": {
            "type": "string",
            "nullable": true,
            "description": "P2 — id of the gear Kit this add-on requires on site. Pass null to clear. Falls back onto Project.requiredKitId for à-la-carte / no-package bookings."
          },
          "requiredCertLevelId": {
            "type": "string",
            "nullable": true,
            "description": "id of the CertificationLevel a technician must hold (PASSED, non-expired) to serve this add-on. Pass null to clear. Null/omitted = no certification required."
          }
        }
      },
      "OverrideReliabilityEventDto": {
        "type": "object",
        "properties": {
          "outcome": {
            "type": "string",
            "enum": [
              "ON_TIME",
              "LATE",
              "MISSED"
            ]
          },
          "missedReason": {
            "type": "string",
            "enum": [
              "TECHNICIAN",
              "NO_ONE_PRESENT"
            ]
          },
          "note": {
            "type": "string",
            "maxLength": 1000,
            "description": "Why the admin/owner is overriding this event."
          },
          "syncVisitStatus": {
            "type": "boolean",
            "description": "Also update the shoot's status to match the corrected record. Default true.",
            "default": true
          }
        }
      },
      "ResetReliabilityBaselineDto": {
        "type": "object",
        "properties": {
          "effectiveFrom": {
            "type": "string",
            "description": "ISO date/time events before this stop counting toward the aggregate. Defaults to now."
          },
          "note": {
            "type": "string",
            "maxLength": 1000,
            "description": "Why the stats are being reset."
          }
        }
      },
      "UpsertTrackingDto": {
        "type": "object",
        "properties": {
          "phase": {
            "type": "object"
          },
          "visitId": {
            "type": "string",
            "description": "The specific shoot (ProjectVisit) this ping belongs to. Optional for\nbackward compatibility with clients that predate per-shoot tracking\n(Phase 1 crew-reliability), and unavoidable for the 98-of-116 production\nprojects that have no `ProjectVisit` row at all. Omitting it keeps the\nlegacy project-level behaviour (keyed on `visitId IS NULL`).\n\nIt NO LONGER skips the gates. It used to skip both the assigned-technician\ncheck and the shoot-window check outright, because both lived inside an\n`if (visit && …)`. The assigned-technician check now runs on this path too,\nagainst the PROJECT's assignment, and the window check degrades to the\nshoot's own local day (a project-level trip has no place in the day's shoot\nsequence to be gated against). Passing a visitId still gets the full\nper-shoot window, which additionally waits out the technician's previous\nsame-day shoot."
          },
          "latitude": {
            "type": "number",
            "minimum": -90,
            "maximum": 90
          },
          "longitude": {
            "type": "number",
            "minimum": -180,
            "maximum": 180
          },
          "headingDegrees": {
            "type": "number",
            "minimum": 0,
            "maximum": 360
          },
          "horizontalAccuracy": {
            "type": "number",
            "description": "`CLLocation.horizontalAccuracy` for THIS ping, in metres — the radius of\nthe 68%-confidence circle around `latitude`/`longitude`.\n\nWHY IT MUST BE DECLARED HERE: the global pipe runs with\n`whitelist: true, forbidNonWhitelisted: true` (main.ts), so an undeclared\nkey does not merely get dropped — it 400s the WHOLE ping, and every client\nin the tree treats a non-2xx trip POST as a retryable failure. Shipping the\nfield client-side without this declaration would take the technician's\nentire trip offline, not degrade one column.\n\nDELIBERATELY UNBOUNDED. CoreLocation reports a NEGATIVE accuracy when the\nfix is invalid, and a cell-tower-only fix legitimately reports thousands of\nmetres. A `@Min(0)`/`@Max(n)` here would turn \"my GPS is poor\" into \"your\ntrip failed\", so the value is accepted as sent and normalised server-side\n(negative / non-finite ⇒ stored as null, i.e. \"quality unknown\").\n\nPersisted to `ProjectTracking.horizontalAccuracyMeters` on every ping, and\nto `ProjectTracking.startAccuracyMeters` for the ping that supplies the\ntrip ORIGIN — payroll bills kilometres against that origin and has to be\nable to reject a 3 km cell-tower fix before it does."
          },
          "etaSeconds": {
            "type": "number",
            "description": "Seconds until estimated arrival at the property. iOS computes\n via MKDirections; pass through verbatim so the customer-side\n UI can render the same number the tech sees.",
            "minimum": 0,
            "maximum": 86400
          },
          "note": {
            "type": "string",
            "description": "Free-text note attached to this ping (\"stuck in traffic on\n the 401\"). Surfaced verbatim to the customer."
          }
        },
        "required": [
          "phase"
        ]
      },
      "RegisterLiveActivityTokenDto": {
        "type": "object",
        "properties": {
          "projectId": {
            "type": "string",
            "minLength": 1
          },
          "activityPushToken": {
            "type": "string",
            "minLength": 1
          },
          "role": {
            "type": "object",
            "enum": [
              "technician",
              "viewer"
            ]
          }
        },
        "required": [
          "projectId",
          "activityPushToken",
          "role"
        ]
      },
      "RegisterLiveActivityStartTokenDto": {
        "type": "object",
        "properties": {
          "pushToStartToken": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "pushToStartToken"
        ]
      },
      "AdminResolveTripDto": {
        "type": "object",
        "properties": {
          "action": {
            "type": "object",
            "description": "`COMPLETE` — assert the trip finished. Requires {@link completedAt} UNLESS\nthe row already has a device-recorded `phaseTimestamps.COMPLETE`.\n`CANCEL`   — end the live state without asserting the technician arrived\n             or worked. Never writes a completion timestamp.",
            "enum": [
              "COMPLETE",
              "CANCEL"
            ]
          },
          "completedAt": {
            "type": "string",
            "description": "The ACTUAL instant the technician finished, ISO-8601. Required for\n`COMPLETE` when the trip never recorded its own completion, because\npayroll derives on-site seconds from ARRIVED → COMPLETE — the server\nrefuses to invent this value. Ignored for `CANCEL`, and ignored for\n`COMPLETE` on a row that already has a device-recorded completion (that\ntimestamp is reused verbatim)."
          },
          "note": {
            "type": "string",
            "description": "Free-text reason, recorded on the audit-log entry alongside the actor.",
            "maxLength": 1000
          }
        },
        "required": [
          "action"
        ]
      },
      "CreateShareRuleDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "role": {
            "type": "object"
          },
          "teamRole": {
            "type": "object",
            "description": "STANDING team role — separate axis from `role` (listing access). Absent =\nAGENT (an ordinary teammate). ADMINISTRATOR/ASSISTANT are the managing /\nnon-managing tiers the portal team offers."
          },
          "scopeOrgId": {
            "type": "string",
            "description": "Limit the standing rule to one fulfilling org. Null/absent = all."
          },
          "label": {
            "type": "string",
            "maxLength": 80
          },
          "applyToExisting": {
            "type": "boolean",
            "description": "Materialise a real ProjectShare for every order I am CURRENTLY the primary\ncustomer of. Each becomes its own auditable, individually-revocable row —\na standing rule never becomes an implicit predicate."
          }
        },
        "required": [
          "email",
          "role"
        ]
      },
      "UpdateShareRuleDto": {
        "type": "object",
        "properties": {
          "role": {
            "type": "object"
          },
          "teamRole": {
            "type": "object"
          },
          "label": {
            "type": "string",
            "maxLength": 80
          }
        }
      },
      "UpdatePortalTeamIdentityDto": {
        "type": "object",
        "properties": {
          "brokerageName": {
            "type": "string",
            "maxLength": 120
          },
          "brokerageLogo": {
            "type": "string",
            "description": "A CDN URL from the media presign-general upload flow.",
            "maxLength": 2000
          }
        }
      },
      "AcceptShareInviteDto": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "maxLength": 200
          }
        },
        "required": [
          "token"
        ]
      },
      "CreateProjectShareDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "description": "Who to share with. Resolved to a User; if none exists, an invite is sent.",
            "format": "email"
          },
          "role": {
            "type": "object"
          },
          "expiresAt": {
            "type": "string",
            "description": "Optional auto-expiry. Enforced LIVE by the predicate, not by a cron."
          }
        },
        "required": [
          "email",
          "role"
        ]
      },
      "UpdateProjectShareDto": {
        "type": "object",
        "properties": {
          "role": {
            "type": "object"
          }
        },
        "required": [
          "role"
        ]
      },
      "CreateInquiryDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "phone": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "name",
          "email"
        ]
      },
      "UpdateInquiryDto": {
        "type": "object",
        "properties": {
          "status": {
            "type": "object"
          }
        }
      },
      "CreateOrganizationDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "type": {
            "type": "object"
          },
          "logoUrl": {
            "type": "string",
            "description": "Optional branding logo (a CDN URL) set at creation — used by agent team orgs.",
            "maxLength": 2000
          }
        },
        "required": [
          "name"
        ]
      },
      "CreateInviteDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "role": {
            "type": "object"
          },
          "inviteType": {
            "type": "object"
          }
        },
        "required": [
          "email"
        ]
      },
      "CreateInviteCodeDto": {
        "type": "object",
        "properties": {
          "inviteType": {
            "type": "object"
          },
          "role": {
            "type": "object"
          }
        }
      },
      "AcceptInviteDto": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string"
          }
        },
        "required": [
          "token"
        ]
      },
      "FeatureEntitlementDto": {
        "type": "object",
        "properties": {
          "granted": {
            "type": "boolean",
            "description": "True when this org may use the feature. The union of its plan grants and its active add-on subscriptions."
          },
          "limit": {
            "type": "number",
            "description": "Maximum quantity this org may hold. ABSENT means unlimited — do not read an absent field as zero."
          }
        },
        "required": [
          "granted"
        ]
      },
      "OrgFeatureEntitlementsDto": {
        "type": "object",
        "properties": {
          "customEmailDomain": {
            "description": "Granted when the org may use \"Custom email domain\".",
            "allOf": [
              {
                "$ref": "#/components/schemas/FeatureEntitlementDto"
              }
            ]
          },
          "customAppDomain": {
            "description": "Granted when the org may use \"Custom domains (white-label)\".",
            "allOf": [
              {
                "$ref": "#/components/schemas/FeatureEntitlementDto"
              }
            ]
          },
          "brochures": {
            "description": "Granted when the org may use \"Brochure generation\".",
            "allOf": [
              {
                "$ref": "#/components/schemas/FeatureEntitlementDto"
              }
            ]
          },
          "apiKeys": {
            "description": "Granted when the org may use \"API key access\".",
            "allOf": [
              {
                "$ref": "#/components/schemas/FeatureEntitlementDto"
              }
            ]
          },
          "webhooks": {
            "description": "Granted when the org may use \"Webhook subscriptions\".",
            "allOf": [
              {
                "$ref": "#/components/schemas/FeatureEntitlementDto"
              }
            ]
          },
          "whitelabelApp": {
            "description": "Granted when the org may use \"Branded mobile app\". Unlike the others this one is CAPPED (limit 1): each branded app costs a bundle id, an Apple Developer slot, CI minutes and a human review, so it is the one entitlement with real marginal cost per tenant.",
            "allOf": [
              {
                "$ref": "#/components/schemas/FeatureEntitlementDto"
              }
            ]
          }
        },
        "required": [
          "customEmailDomain",
          "customAppDomain",
          "brochures",
          "apiKeys",
          "webhooks",
          "whitelabelApp"
        ]
      },
      "OrgEntitlementsDto": {
        "type": "object",
        "properties": {
          "plan": {
            "type": "string",
            "enum": [
              "FREE",
              "ENTERPRISE",
              "FOUNDER"
            ],
            "description": "Plan label for display. FOUNDER wins over ENTERPRISE when both apply — it names the arrangement, while isEnterprise names the access."
          },
          "isEnterprise": {
            "type": "boolean",
            "description": "True when the org holds the ENTERPRISE (or FOUNDER) plan AND is paying. This is the PLAN question, not a feature question: an active add-on SKU grants a feature to an org that reads false here, so clients must gate on features[key].granted, never on this."
          },
          "subscriptionActive": {
            "type": "boolean",
            "description": "Enterprise AND actually paying (active / trialing / past_due, a card-less granted trial, or FOUNDER). False for a lapsed enterprise, which keeps every feature but falls back to the per-transaction fee."
          },
          "transactionFeePct": {
            "type": "number",
            "nullable": true,
            "description": "The percentage a charge would actually take right now: 0 when exempt, otherwise the effective rate including any per-org override. Measured through computePlatformFeeCents, so it cannot disagree with what Stripe takes. Always a number on the wire; null is reserved for clients modelling \"not loaded yet\"."
          },
          "features": {
            "description": "One { granted, limit? } per gated feature — the union of the plan grants and any active add-on SKU subscriptions. Clients gate the UI on these so a Free org is shown an upgrade path instead of a button that 403s. Gate on `granted`, never on which SKU was sold: SKUs are repriced and rebundled, entitlement keys are not.",
            "allOf": [
              {
                "$ref": "#/components/schemas/OrgFeatureEntitlementsDto"
              }
            ]
          }
        },
        "required": [
          "plan",
          "isEnterprise",
          "subscriptionActive",
          "transactionFeePct",
          "features"
        ]
      },
      "UpdateOrganizationSettingsDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "legalName": {
            "type": "string"
          },
          "slug": {
            "type": "string",
            "description": "═══ THIS FIELD IS A DNS LABEL, NOT A DISPLAY STRING ══════════════════════\n\nIt becomes `<slug>.vremly.com`, which `registerSubdomain` /\n`removeSubdomain` interpolate into a Vercel management-API URL PATH. As a\nbare `@IsString()` it accepted `../../../../v9/projects/<projectId>#`,\nwhich a URL parser resolves to `/v9/projects/<projectId>` — on the DELETE,\nthat is \"delete the entire Vercel project\", carrying VERCEL_API_TOKEN, and\nit takes vremly.com and app.vremly.com down together. Reachable by any\nauthenticated user: create a COMPANY org (you become OWNER), PATCH the slug\nto arm it, PATCH again so `slugChanged` fires the teardown.\n\n`@Matches` is the SOURCE half of that fix. `assertProvisionableHost` at\nevery outbound Vercel URL is the sink half, and neither assumes the other\nran. Reserved-name screening is applied in the service, against the SAME\nexported list `generateUniqueSlug` uses on create — see org-slug.ts.\n\n`@MaxLength` is redundant against the pattern and kept anyway: a length\nviolation reports as a length violation, which is what the person renaming\ntheir company actually needs to be told.",
            "maxLength": 63
          },
          "logoUrl": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "brandColor": {
            "type": "string"
          },
          "websiteUrl": {
            "type": "string"
          },
          "termsOfServiceUrl": {
            "type": "string"
          },
          "privacyPolicyUrl": {
            "type": "string"
          },
          "sessionRecordingEnabled": {
            "type": "boolean",
            "description": "The org's own switch for third-party session recording (Microsoft\nClarity). False stops the recorder for this organisation's staff AND its\nportal customers, everywhere, without a deploy — see\nOrganization.sessionRecordingEnabled in schema.prisma and the fourth gate\nin components/shared/analytics/ClarityInit.tsx.\n\nSits next to the legal links deliberately: the person editing an\norganisation's terms and privacy URLs is the person who needs to find it."
          },
          "phone": {
            "type": "string"
          },
          "primaryEmail": {
            "type": "string"
          },
          "supportEmail": {
            "type": "string"
          },
          "smsNumber": {
            "type": "string"
          },
          "addressLine1": {
            "type": "string"
          },
          "addressLine2": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "timezone": {
            "type": "string"
          },
          "taxRegistrationNumber": {
            "type": "string"
          },
          "qstNumber": {
            "type": "string"
          },
          "pstNumber": {
            "type": "string"
          },
          "manualTaxRatePercent": {
            "type": "number",
            "nullable": true,
            "minimum": 0,
            "maximum": 100
          },
          "manualTaxLabel": {
            "type": "string",
            "maxLength": 60
          },
          "returningCustomerMigrationEnabled": {
            "type": "boolean"
          },
          "defaultCurrency": {
            "type": "string"
          },
          "serviceArea": {
            "type": "object"
          },
          "bookingStartTime": {
            "type": "string"
          },
          "bookingEndTime": {
            "type": "string"
          },
          "capacityNotes": {
            "type": "string"
          },
          "travelRadiusKm": {
            "type": "number"
          },
          "cancellationWindowHours": {
            "type": "number"
          },
          "assignmentStrategy": {
            "type": "string"
          },
          "autoAssignNewOrders": {
            "type": "boolean"
          },
          "aiCopywritingEnabled": {
            "type": "boolean"
          },
          "videoScriptsEnabled": {
            "type": "boolean"
          },
          "videoScriptDefaultLengthSeconds": {
            "type": "number",
            "nullable": true,
            "description": "═══ THE COMPANY'S HOUSE STYLE FOR AI VIDEO SCRIPTS ══════════════════════\n\nThe five script controls, set ONCE for the company instead of re-picked on\nevery generation: length, tone, pace, what the hook leads with, and how far\nthe writing may depart from a literal description. A per-generation request\noverrides any of them individually — the resolution order is REQUEST →\nTHESE → the built-in, PER FIELD, in `resolveScriptControls`.\n\nTHIS IS THE ONLY WRITE PATH THEY HAVE, and it is the reason they are here\nrather than only on the generate request. The ORDER-TRIGGERED generation\nhas no user to ask — it fires from an order-creation hook or a sweep tick —\nso it resolves against these columns and nothing else. Without a route a\ncompany that shoots luxury listings could set its house style only through\na psql session, which is how `allowCustomerWebsiteEdits` above spent months\npinned to its default.\n\n`@IsIn` AGAINST THE SHARED ALLOWLISTS, imported rather than restated. These\nvalues are INTERPOLATED INTO AN LLM SYSTEM PROMPT, so the set of legal\nstrings has exactly one definition (`video-script-controls.ts`) and a\nsecond copy here is how a value this route accepts stops being a value the\nprompt understands. Out of range is a 400, never a coercion — and\n`resolveScriptControls` checks these columns AGAIN when it reads them, so a\nvalue written by a restored backup still cannot reach the model.\n\nNULL IS MEANINGFUL AND STAYS REACHABLE: it means \"this company never chose\none\", which is a different fact from \"chose the same thing the built-in\nsays\" and stays different if a built-in is ever changed. `@ValidateIf`\nadmits an explicit `null` so a company can clear a choice; `@IsOptional`\nalone would make `null` a validation error and leave no way back.",
            "enum": [
              30,
              60,
              90,
              120
            ]
          },
          "videoScriptDefaultTone": {
            "type": "object",
            "nullable": true,
            "enum": [
              "professional",
              "warm",
              "energetic",
              "luxury"
            ]
          },
          "videoScriptDefaultPace": {
            "type": "object",
            "nullable": true,
            "enum": [
              "calm",
              "measured",
              "fast"
            ]
          },
          "videoScriptDefaultLeadWith": {
            "type": "object",
            "nullable": true,
            "enum": [
              "lifestyle",
              "features",
              "location"
            ]
          },
          "videoScriptDefaultBoldness": {
            "type": "object",
            "nullable": true,
            "description": "safe | balanced | bold. IT IS NOT A TEMPERATURE SETTING and must never be\nmade into one — see the docblock on `SCRIPT_BOLDNESS`. It changes the\ninstruction language the model is given; grounding is absolute at every\nvalue, including `bold`.",
            "enum": [
              "safe",
              "balanced",
              "bold"
            ]
          },
          "allowCustomerWebsiteEdits": {
            "type": "boolean",
            "description": "\"May the listing agent edit their own delivered property website?\"\n\nADDED 2026-08-13. The column has existed and been ENFORCED since the\ncustomer-website-editing work (`listings.service.ts` refuses the edit when\nit is false), but it was in no DTO and on no route — the only way to switch\nit off was a psql session, and every org therefore ran on the `true`\ndefault. Locking it down per listing\n(`ListingWebsiteConfig.customerEditsLockedAt`) was the only reachable\nsubstitute, one listing at a time.\n\nDeliberately NOT given a new default here: the column stays\n`@default(true)`, so declaring it changes nothing until somebody flips it."
          },
          "checkoutWhitelabel": {
            "type": "boolean"
          },
          "availabilityMode": {
            "type": "string"
          },
          "bufferTimeMinutes": {
            "type": "number"
          },
          "calendarAllDayFreeBlocks": {
            "type": "boolean"
          },
          "calendarAllDayBusyBlocks": {
            "type": "boolean"
          },
          "calendarTentativeBlocks": {
            "type": "boolean"
          },
          "minimumBookingNoticeHours": {
            "type": "number"
          },
          "shootingSlaHours": {
            "type": "number",
            "minimum": 1,
            "maximum": 720
          },
          "editingSlaHours": {
            "type": "number",
            "minimum": 1,
            "maximum": 2160
          },
          "shootReminderLeadHours": {
            "type": "number",
            "minimum": 2,
            "maximum": 72
          },
          "overallTurnaroundHours": {
            "type": "number",
            "nullable": true,
            "minimum": 1,
            "maximum": 2160
          },
          "slaAutoCompleteDeliverables": {
            "type": "boolean"
          },
          "aiVisibleDisclosure": {
            "type": "boolean"
          },
          "slaSlackChannelId": {
            "type": "string",
            "nullable": true,
            "maxLength": 32
          },
          "onSiteRequirements": {
            "type": "object"
          },
          "paymentMode": {
            "type": "object"
          },
          "paymentRoutingMode": {
            "type": "object",
            "description": "HOW this org's customer payments move. OPERATIONAL ONLY.\n\nIt does NOT and MUST NOT decide whether the platform charges its fee.\nThat is `computePlatformFeeCents` (common/plans.ts), which reads the org's\nplan and exemptions — never a preference the org sets about itself. Until\n2026-08-29 `DIRECT_STRIPE` dropped `application_fee_amount` in all three\ncharge paths, so this one field was a self-serve waiver of the 3%: the org\nkept `transfer_data.destination` (100% of the money) and paid nothing.\n\n`DIRECT_STRIPE` additionally requires staff to have provisioned real\ndirect settlement (`stripeDirectCharges` / `stripeStandaloneMode`, neither\nof which is in this DTO); the service 403s otherwise."
          },
          "captureStrategy": {
            "type": "object",
            "description": "WHEN THE CUSTOMER'S CARD IS CHARGED at booking checkout.\n\n  IMMEDIATE    — charge at checkout.\n  MANUAL_LATER — authorize at checkout; someone captures on delivery via\n                 `POST /orders/:projectId/capture`. THE DEFAULT, and the\n                 behaviour every checkout had hardcoded before this setting\n                 existed. A card authorization lapses after roughly 7 days\n                 and nothing captures automatically, so an org on this\n                 setting has a real obligation: capture, or lose the sale.\n\n`paymentRoutingMode: ESCROW_LIKE` forces manual capture regardless of what\nis sent here — that routing mode *is* \"hold the money until the work\nlands\". Setting IMMEDIATE alongside it is accepted and has no effect; the\nUI should say so rather than pretend the pair is contradictory."
          },
          "workforceType": {
            "type": "object"
          },
          "bookingQuestions": {
            "type": "object"
          },
          "bookingFormConfig": {
            "type": "object"
          },
          "autoProgressJobs": {
            "type": "boolean"
          },
          "customerEmailNotifications": {
            "type": "boolean"
          },
          "customerSmsNotifications": {
            "type": "boolean"
          },
          "autoSendInvoiceOnOrder": {
            "type": "boolean",
            "description": "Bill at booking. True (the default, and what the platform used to\nhardcode) emails the customer their auto-created invoice the moment an\norder is placed. False still creates the invoice — it just stays a DRAFT\nuntil staff send it or the project is delivered."
          },
          "surveyEnabled": {
            "type": "boolean"
          },
          "reviewPromptEnabled": {
            "type": "boolean"
          },
          "reviewPromptThreshold": {
            "type": "number",
            "minimum": 1,
            "maximum": 5
          },
          "loyaltyEnabled": {
            "type": "boolean"
          },
          "daysOnMarketAlertsEnabled": {
            "type": "boolean"
          },
          "daysOnMarketMilestones": {
            "type": "array",
            "items": {
              "type": "number"
            }
          },
          "cancellationFeesEnabled": {
            "type": "boolean"
          },
          "cancellationFeeCents": {
            "type": "number"
          },
          "lateCancellationFeeCents": {
            "type": "number"
          },
          "requireCardAtBooking": {
            "type": "boolean"
          },
          "bookingCardCopy": {
            "type": "string"
          },
          "cancellationInsuranceEnabled": {
            "type": "boolean"
          },
          "cancellationInsurancePriceCents": {
            "type": "number"
          },
          "requestSchedulingFeeEnabled": {
            "type": "boolean"
          },
          "requestSchedulingFeeCents": {
            "type": "number"
          },
          "multiAppointmentsEnabled": {
            "type": "boolean"
          },
          "multiVisitFeeCents": {
            "type": "number",
            "nullable": true
          },
          "autoApproveAdditionalShoots": {
            "type": "boolean"
          },
          "welcomeScreen": {
            "type": "object",
            "nullable": true
          },
          "cancellationRequestsEnabled": {
            "type": "boolean"
          },
          "cancellationApprovalGraceDays": {
            "type": "number"
          },
          "autoHideRedundantAddOns": {
            "type": "boolean"
          },
          "buildYourOwnEnabled": {
            "type": "boolean"
          },
          "aiPipelineEnabled": {
            "type": "boolean"
          },
          "scanEnabled": {
            "type": "boolean"
          },
          "l2Enabled": {
            "type": "boolean"
          },
          "shiftsEnabled": {
            "type": "boolean"
          },
          "teamFeaturesEnabled": {
            "type": "boolean"
          },
          "teamTrainingFeaturesEnabled": {
            "type": "boolean"
          },
          "teamEquipmentFeaturesEnabled": {
            "type": "boolean"
          },
          "marketTamCents": {
            "type": "number"
          },
          "marketSamCents": {
            "type": "number"
          },
          "marketNotes": {
            "type": "string"
          },
          "tourDomain": {
            "type": "string"
          }
        }
      },
      "CreateCancellationInsurancePlanDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 160,
            "description": "Plan name (e.g. \"Basic\", \"Premium\")"
          },
          "description": {
            "type": "string",
            "description": "Plan description shown at booking"
          },
          "price": {
            "type": "number",
            "minimum": 0,
            "description": "Base premium in cents"
          },
          "currency": {
            "type": "string",
            "maxLength": 12,
            "description": "Currency code (default: usd)"
          },
          "serviceAreaIds": {
            "description": "Service area IDs this plan is available in (empty = all areas)",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "priceByAreaCents": {
            "type": "object",
            "description": "Per-service-area premium overrides in cents, keyed by service-area id ({ [serviceAreaId]: cents }). Areas not listed fall back to `price`."
          },
          "coverageWindowHours": {
            "type": "number",
            "minimum": 0,
            "description": "Coverage window in hours. null = covers any cancellation made before the shoot starts (no hour limit); N = covers cancellations made within N hours of the scheduled shoot."
          },
          "coversOnSite": {
            "type": "boolean",
            "description": "When true, also covers cancellations after the tech is on-site / after the scheduled start time."
          },
          "displayOrder": {
            "type": "number",
            "minimum": 0,
            "description": "Display order"
          }
        },
        "required": [
          "name",
          "price"
        ]
      },
      "UpdateCancellationInsurancePlanDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 160,
            "description": "Plan name"
          },
          "description": {
            "type": "string",
            "description": "Plan description"
          },
          "price": {
            "type": "number",
            "minimum": 0,
            "description": "Base premium in cents"
          },
          "currency": {
            "type": "string",
            "maxLength": 12,
            "description": "Currency code"
          },
          "serviceAreaIds": {
            "description": "Service area IDs this plan is available in (empty = all areas)",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "priceByAreaCents": {
            "type": "object",
            "description": "Per-service-area premium overrides in cents, keyed by service-area id."
          },
          "coverageWindowHours": {
            "type": "number",
            "minimum": 0,
            "description": "Coverage window in hours. null = covers any pre-shoot cancellation; N = within N hours of the shoot."
          },
          "coversOnSite": {
            "type": "boolean",
            "description": "When true, also covers cancellations after the tech is on-site."
          },
          "isActive": {
            "type": "boolean",
            "description": "Whether the plan is active"
          },
          "displayOrder": {
            "type": "number",
            "minimum": 0,
            "description": "Display order"
          }
        }
      },
      "UpdateMemberRoleDto": {
        "type": "object",
        "properties": {
          "role": {
            "type": "object"
          }
        },
        "required": [
          "role"
        ]
      },
      "UpdateMemberEmploymentTypeDto": {
        "type": "object",
        "properties": {
          "employmentType": {
            "type": "object"
          }
        },
        "required": [
          "employmentType"
        ]
      },
      "UpdateMemberAvailabilityDto": {
        "type": "object",
        "properties": {
          "isAvailable": {
            "type": "boolean"
          },
          "availabilityNote": {
            "type": "string",
            "nullable": true
          },
          "autoDeclineBookings": {
            "type": "boolean",
            "description": "REMOVED FEATURE — accepted and ignored. See AvailabilityStatusDto for the\nfull reasoning; kept only so a stale client's request is not rejected\nwholesale by `forbidNonWhitelisted: true`."
          }
        },
        "required": [
          "isAvailable"
        ]
      },
      "UpdateMemberServiceProviderDto": {
        "type": "object",
        "properties": {
          "isServiceProvider": {
            "type": "boolean"
          }
        },
        "required": [
          "isServiceProvider"
        ]
      },
      "UpdateMemberServiceAreasDto": {
        "type": "object",
        "properties": {
          "serviceAreaIds": {
            "description": "Service-area ids from Organization.serviceArea. Empty = derive from the member's home base.",
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "serviceAreaIds"
        ]
      },
      "UpdateMemberEditorSpecialtiesDto": {
        "type": "object",
        "properties": {
          "editorMediaTypes": {
            "type": "array",
            "description": "Media types this member is auto-routed. Empty = not auto-routable (they can still be assigned by hand, and can still complete anything).",
            "items": {
              "type": "string",
              "enum": [
                "PHOTO",
                "VIDEO",
                "FLOORPLAN",
                "DOCUMENT",
                "VIRTUAL_TOUR",
                "PROPERTY_WEBSITE",
                "BROCHURE"
              ]
            }
          }
        },
        "required": [
          "editorMediaTypes"
        ]
      },
      "EditorRoutingModeDto": {
        "type": "object",
        "properties": {
          "mediaType": {
            "type": "string",
            "enum": [
              "PHOTO",
              "VIDEO",
              "FLOORPLAN",
              "DOCUMENT",
              "VIRTUAL_TOUR",
              "PROPERTY_WEBSITE",
              "BROCHURE"
            ]
          },
          "mode": {
            "type": "string",
            "enum": [
              "ALL",
              "SPLIT"
            ],
            "description": "ALL = attach every editor who lists this type. SPLIT = attach the least-loaded one."
          },
          "slaHours": {
            "type": "number",
            "nullable": true,
            "description": "Per-media-type production budget in hours. Null clears it and falls back to the org editing budget.",
            "minimum": 1,
            "maximum": 8760
          }
        },
        "required": [
          "mediaType",
          "mode"
        ]
      },
      "UpdateEditorRoutingDto": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "Master switch. Off (the default for every org) means nothing is auto-assigned."
          },
          "modes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EditorRoutingModeDto"
            }
          }
        }
      },
      "UpdateMemberCalendarColorDto": {
        "type": "object",
        "properties": {
          "calendarColor": {
            "type": "string",
            "nullable": true,
            "pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$",
            "description": "Hex colour for this member's calendar events (e.g. \"#0369a1\"), or null to fall back to the auto-assigned palette colour.",
            "example": "#0369a1"
          }
        }
      },
      "UpdateMemberRestrictProjectsDto": {
        "type": "object",
        "properties": {
          "restrictToAssignedProjects": {
            "type": "boolean"
          }
        },
        "required": [
          "restrictToAssignedProjects"
        ]
      },
      "UpdateMemberSplitDto": {
        "type": "object",
        "properties": {
          "splitType": {
            "type": "object"
          },
          "splitPercentage": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "splitFlatAmountCents": {
            "type": "number",
            "minimum": 0,
            "maximum": 1000000
          },
          "splitHourlyRateCents": {
            "type": "number",
            "minimum": 0,
            "maximum": 1000000
          },
          "splitHourlyHours": {
            "type": "number",
            "minimum": 0
          },
          "payrollMileageRateCents": {
            "type": "number",
            "nullable": true,
            "description": "Per-member mileage reimbursement, in cents per distance unit — `null` clears\nthe override so the member falls back to the org default.\n\n`OrganizationMember.payrollMileageRateCents` is READ on every compute\n(`member ?? org ?? 0`) and effective-dated into `MemberPayRateVersion`, but\nit had no API at all: it was absent from this DTO, and `whitelist: true`\nsilently strips unknown body fields, so a client sending it got a 200 and no\nchange. Its only writer was the CSV pay-structure import — a technician on a\nnegotiated mileage rate could only be set up by uploading a file.\n\nIndependent of `splitType`: travel is reimbursed the same way whether the\nmember is PERCENTAGE, HOURLY or FLAT, so unlike the split fields this is not\ncleared when the split type changes.",
            "minimum": 0,
            "maximum": 1000000
          },
          "payrollDriveTimeRateCents": {
            "type": "number",
            "nullable": true,
            "description": "Per-member drive-time rate, in cents per hour — `null` clears the override so\nthe member falls back to the org default. Same story as\n`payrollMileageRateCents`: read at compute time, effective-dated, and until\nnow unreachable from any API.",
            "minimum": 0,
            "maximum": 1000000
          },
          "payCurrency": {
            "type": "string",
            "nullable": true,
            "description": "WHICH CURRENCY THIS PAYEE IS PAID IN — ISO-4217, or `null` to clear the\noverride so they fall back to the org's `defaultCurrency`.\n\nSits here beside the split and travel rates because it is a fact about the\nsame person's compensation, and beside `employmentType` on the screen for\nthe same reason: both describe how the org is engaged with them. An org that\npays Canadian technicians in CAD and outsourced editors in USD sets this on\nthe editors and leaves everybody else alone.\n\nThree states, all meaningful and all distinct on the wire — the same shape\nthe travel-rate overrides above use, and the same reason: `whitelist: true`\nstrips an unknown key silently, so the client must send exactly one of\nomitted / `null` / a value, and the service branches on `!== undefined`.\n\n`@ValidateIf` lets an explicit `null` through: `@IsOptional()` alone would\naccept it, but `@Length` would then also have to (it does not), so the\nclear-the-override case needs the guard to be skipped rather than satisfied.",
            "minLength": 3,
            "maxLength": 3
          }
        },
        "required": [
          "splitType"
        ]
      },
      "UpdateMyAddressDto": {
        "type": "object",
        "properties": {
          "addressLine1": {
            "type": "string"
          },
          "addressLine2": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          }
        }
      },
      "UpdateMemberAddressDto": {
        "type": "object",
        "properties": {
          "isServiceProvider": {
            "type": "boolean"
          },
          "addressLine1": {
            "type": "string"
          },
          "addressLine2": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          }
        }
      },
      "SaveAiCredentialDto": {
        "type": "object",
        "properties": {
          "apiKey": {
            "type": "string",
            "minLength": 8,
            "maxLength": 500,
            "description": "The provider API key. Stored encrypted (AES-256-GCM); never returned by any read route.",
            "example": "sk-ant-…"
          },
          "label": {
            "type": "string",
            "maxLength": 120,
            "description": "Your own name for this key, shown on the settings screen.",
            "example": "Marketing team key"
          }
        },
        "required": [
          "apiKey"
        ]
      },
      "SaveStripeCredentialDto": {
        "type": "object",
        "properties": {
          "secretKey": {
            "type": "string",
            "minLength": 12,
            "maxLength": 500,
            "description": "The Stripe secret key (sk_live_…, sk_test_…, or a restricted rk_… key). Stored encrypted (AES-256-GCM) and NEVER returned by any route, in any form. A restricted key scoped to payments is preferred over a full secret key.",
            "example": "sk_live_…"
          },
          "publishableKey": {
            "type": "string",
            "minLength": 12,
            "maxLength": 500,
            "description": "The publishable key from the SAME Stripe account and the SAME mode. It is published to browsers and to the mobile SDK, so it is not a secret — but it is stored beside the secret key because the pair must not drift.",
            "example": "pk_live_…"
          },
          "webhookSecret": {
            "type": "string",
            "minLength": 12,
            "maxLength": 500,
            "description": "The signing secret (whsec_…) of the webhook endpoint you created in your own Stripe dashboard pointing at the URL this route returns. Stored encrypted and never returned. Omit it on a key rotation to leave the existing one in place.",
            "example": "whsec_…"
          },
          "label": {
            "type": "string",
            "maxLength": 120,
            "description": "Your own name for this key, shown on the settings screen.",
            "example": "Live key, rotated Aug 2026"
          }
        },
        "required": [
          "secretKey",
          "publishableKey"
        ]
      },
      "SetStripeStandaloneModeDto": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean",
            "description": "True to route this company's payments through its own Stripe account and key. False returns them to the platform account. Reversible at any time; the stored credentials are kept either way."
          }
        },
        "required": [
          "enabled"
        ]
      },
      "UpdateRolePermissionDto": {
        "type": "object",
        "properties": {
          "role": {
            "type": "object",
            "enum": [
              "ADMIN",
              "PROJECT_MANAGER",
              "TECHNICIAN",
              "EDITOR"
            ]
          },
          "capability": {
            "type": "string"
          },
          "readAccess": {
            "type": "object",
            "enum": [
              "NONE",
              "ASSIGNED",
              "ALL"
            ]
          },
          "writeAccess": {
            "type": "object",
            "enum": [
              "NONE",
              "ASSIGNED",
              "ALL"
            ]
          }
        },
        "required": [
          "role",
          "capability",
          "readAccess",
          "writeAccess"
        ]
      },
      "CustomerNotificationChangeDto": {
        "type": "object",
        "properties": {
          "event": {
            "type": "string"
          },
          "channel": {
            "type": "object",
            "enum": [
              "EMAIL",
              "SMS",
              "PUSH"
            ]
          },
          "enabled": {
            "type": "boolean"
          }
        },
        "required": [
          "event",
          "channel",
          "enabled"
        ]
      },
      "UpdateCustomerNotificationsDto": {
        "type": "object",
        "properties": {
          "changes": {
            "minItems": 1,
            "maxItems": 200,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CustomerNotificationChangeDto"
            }
          }
        },
        "required": [
          "changes"
        ]
      },
      "GrantOrgTrialDto": {
        "type": "object",
        "properties": {
          "days": {
            "type": "number",
            "minimum": 1,
            "maximum": 730,
            "description": "Trial length in whole days from now. Mutually exclusive with endsAt."
          },
          "endsAt": {
            "type": "string",
            "description": "Explicit trial end as an ISO-8601 timestamp. Mutually exclusive with days. Must be in the future and no more than 730 days out — the same ceiling `days` carries, enforced in OrgTrialsService."
          },
          "note": {
            "type": "string",
            "maxLength": 500,
            "description": "Why this trial was granted — shown on the staff trials report."
          }
        }
      },
      "RevokeOrgTrialDto": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "maxLength": 500
          }
        }
      },
      "CreateInventoryItemDto": {
        "type": "object",
        "properties": {}
      },
      "UpdateInventoryItemDto": {
        "type": "object",
        "properties": {}
      },
      "AddUnitsDto": {
        "type": "object",
        "properties": {}
      },
      "AdjustUnitStatusesDto": {
        "type": "object",
        "properties": {}
      },
      "UpdateUnitDto": {
        "type": "object",
        "properties": {}
      },
      "CreateInventoryCategoryDto": {
        "type": "object",
        "properties": {}
      },
      "UpdateInventoryCategoryDto": {
        "type": "object",
        "properties": {}
      },
      "CreateKitDto": {
        "type": "object",
        "properties": {}
      },
      "UpdateKitDto": {
        "type": "object",
        "properties": {}
      },
      "AssignKitDto": {
        "type": "object",
        "properties": {}
      },
      "MigrateReturningCustomersDto": {
        "type": "object",
        "properties": {
          "customerIds": {
            "maxItems": 5000,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "CreateCustomerDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "phone": {
            "type": "string",
            "description": "REQUIRED — a customer added through this route is a customer being signed\nup, and whoever books their shoot must be able to phone them.\n\nSCOPE, deliberately narrow. This is `POST /customers` only: the four\nstaff-facing \"Add customer\" forms and the iOS invite sheet. It does NOT\ntouch the paths that legitimately have no phone to offer, all of which\nstay permissive on purpose:\n\n - `UpdateCustomerDto` (PATCH /customers/:id) — an existing phone-less\n   customer must stay editable for name, notes, brokerage and everything\n   else. Demanding a phone in order to save an unrelated field would lock\n   staff out of records they can edit today.\n - `POST /customers/import`, the bulk importer and the Aryeo migration —\n   these call `CustomersService.findOrCreateCustomer` directly and never\n   construct this DTO. A historical migration of thousands of contacts,\n   many with no number on file, must not fail row by row.\n - Every auto-provision path (invite acceptance, JIT provisioning in\n   JwtStrategy, loyalty streak rows, customer-portal lazy creation, demo\n   seeding). Several of those run inside token validation and have no\n   request body at all, so there is nowhere to put a phone; enforcing\n   there would lock existing users out of the API entirely.\n\n`OrganizationCustomer.phone` itself stays NULLABLE in the schema. A NOT\nNULL column would fail outright against existing null rows, and\n`prisma migrate deploy` runs in one transaction BEFORE the new image\nserves, so it would also break the auto-provision paths above mid-deploy.\nThe requirement belongs at the request boundary, not on the column.\n\nTightening this in a single deploy is safe because `phone` was ALREADY a\ndeclared field here — under `forbidNonWhitelisted` a brand-new field would\nhave 400'd the whole request whichever side shipped first."
          },
          "notes": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "photoUrl": {
            "type": "string"
          },
          "brokerageName": {
            "type": "string"
          },
          "brokerageLogo": {
            "type": "string"
          },
          "socialLinks": {
            "type": "object"
          }
        },
        "required": [
          "phone"
        ]
      },
      "UpdateCustomerDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "phone": {
            "type": "string"
          },
          "additionalEmails": {
            "description": "Secondary email addresses. The `email` above stays the primary/default.",
            "type": "array",
            "items": {
              "type": "string",
              "format": "email"
            }
          },
          "additionalPhones": {
            "description": "Secondary phone numbers. The `phone` above stays the primary/default.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "notes": {
            "type": "string"
          },
          "photoUrl": {
            "type": "string"
          },
          "brokerageName": {
            "type": "string"
          },
          "brokerageLogo": {
            "type": "string"
          },
          "socialLinks": {
            "type": "object"
          }
        }
      },
      "BulkInviteCustomersDto": {
        "type": "object",
        "properties": {
          "customerIds": {
            "minItems": 1,
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "customerIds"
        ]
      },
      "BulkDeleteCustomersDto": {
        "type": "object",
        "properties": {
          "customerIds": {
            "minItems": 1,
            "maxItems": 1000,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "customerIds"
        ]
      },
      "ImportCustomerRowDto": {
        "type": "object",
        "properties": {
          "row": {
            "type": "number",
            "description": "The row's line number IN THE OPERATOR'S FILE (header = 1), not its index in\nthis request. The client chunks a large file across several requests, so an\nindex would report \"row 3\" for row 1003 and send someone to the wrong line.",
            "minimum": 1
          },
          "id": {
            "type": "string",
            "description": "The `id` column the export writes. It is the STRONGEST match this importer\nhas and the reason a round trip is exact: a directory row with no email and\nno phone has no other stable identity, so without it re-importing an\nunchanged export would mint a fresh copy of every such customer."
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "teamName": {
            "type": "string",
            "description": "The team this row's customer belongs to. Stored ONCE per team, on the\nowner's `OrganizationCustomer.brokerageName` — the portal team identity the\nowner sets themselves — never copied onto their rules."
          },
          "teamOwnerEmail": {
            "type": "string",
            "description": "WHO THE TEAM BELONGS TO. A sharing team is owner-centric — it is literally\n\"this user plus everyone named on their standing rules\" — so a team cannot\nbe created without naming its owner, and a row that names a team without a\nresolvable owner is a bad row, never a silent skip."
          },
          "teamRole": {
            "type": "string",
            "description": "ADMINISTRATOR | AGENT | ASSISTANT, or OWNER on the owner's own row."
          }
        },
        "required": [
          "row"
        ]
      },
      "ImportCustomersDto": {
        "type": "object",
        "properties": {
          "rows": {
            "description": "Capped at 1000 to match BulkDeleteCustomersDto's ceiling; the webapp chunks\na larger file into consecutive requests and aggregates the results, which\nis safe because every step of the import is idempotent.",
            "minItems": 1,
            "maxItems": 1000,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ImportCustomerRowDto"
            }
          }
        },
        "required": [
          "rows"
        ]
      },
      "SendTeamInvitesDto": {
        "type": "object",
        "properties": {
          "inviteIds": {
            "minItems": 1,
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "inviteIds"
        ]
      },
      "OutstandingTeamInvitesDto": {
        "type": "object",
        "properties": {
          "ownerEmails": {
            "minItems": 1,
            "maxItems": 500,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "ownerEmails"
        ]
      },
      "SendPortalInvitesDto": {
        "type": "object",
        "properties": {
          "customerIds": {
            "minItems": 1,
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "customerIds"
        ]
      },
      "StartReturningCustomerDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "orgId": {
            "type": "string"
          }
        },
        "required": [
          "email",
          "orgId"
        ]
      },
      "SetPasswordReturningCustomerDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "code": {
            "type": "string"
          },
          "password": {
            "type": "string",
            "minLength": 6
          },
          "orgId": {
            "type": "string"
          }
        },
        "required": [
          "email",
          "code",
          "password",
          "orgId"
        ]
      },
      "CreateTeamDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          }
        },
        "required": [
          "name"
        ]
      },
      "AddTeamMemberDto": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "description": "Either an existing account or an email address. An account joins\nimmediately; an unknown email is INVITED and joins on accept.",
            "format": "uuid"
          },
          "email": {
            "type": "string",
            "description": "Deliberately NOT `@IsEmail()`. The roster UI can save while someone is\nmid-type, and a 400 on a half-typed address is worse than a row that\nnever resolves — the service lower-cases and looks it up, and an address\nthat matches nobody becomes an invitation rather than an error.",
            "maxLength": 320
          },
          "role": {
            "type": "object",
            "description": "OWNER is rejected by the service: ownership is transferred explicitly, not\nhanded out by adding a second one."
          }
        }
      },
      "TransferTeamOwnershipDto": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "description": "The member who becomes OWNER. They must already be on the team.",
            "format": "uuid"
          }
        },
        "required": [
          "userId"
        ]
      },
      "SetProjectOwnerTeamDto": {
        "type": "object",
        "properties": {
          "teamId": {
            "type": "string",
            "nullable": true,
            "description": "Omit or send null to take the listing back out of its team.",
            "format": "uuid"
          }
        }
      },
      "SchedulingContactDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          }
        },
        "required": [
          "name"
        ]
      },
      "CreatePublicOrderDto": {
        "type": "object",
        "properties": {
          "orgId": {
            "type": "string"
          },
          "contactName": {
            "type": "string"
          },
          "contactEmail": {
            "type": "string",
            "format": "email"
          },
          "contactPhone": {
            "type": "string",
            "description": "REQUIRED. This is the customer-facing public booking form — the single\nbiggest way an OrganizationCustomer comes into existence, and the one the\nowner asked about: a provider was ending up with shoots booked by people\nthey had no way to phone.\n\nEnforced on the DTO so it is a real requirement rather than a frontend\nasterisk. Safe to tighten in one deploy because `contactPhone` was ALREADY\na declared field — under `forbidNonWhitelisted` a newly-added field would\n400 the entire booking whichever side deployed first; tightening an\nexisting one only ever rejects a booking that was genuinely missing a\nphone.\n\nRetroactively safe: this validates the REQUEST, not the stored record.\n`OrganizationCustomer.phone` stays nullable, so the thousands of existing\ncustomers with no phone keep working everywhere. A returning customer who\nbooks again simply fills the field in — and orders.service only ever\nbackfills a null phone, it never overwrites one."
          },
          "addressLine1": {
            "type": "string"
          },
          "addressLine2": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "scheduledTime": {
            "type": "string"
          },
          "schedulingMode": {
            "type": "string",
            "enum": [
              "scheduled",
              "requested"
            ]
          },
          "packageId": {
            "type": "string"
          },
          "addOnIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "customAddOnIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "addOnQuantities": {
            "type": "object"
          },
          "requiredKitId": {
            "type": "string",
            "nullable": true
          },
          "selectedPackageVariantId": {
            "type": "string"
          },
          "selectedAddOnVariantIds": {
            "type": "object"
          },
          "bookingAnswers": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "notes": {
            "type": "string"
          },
          "mediaTypes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "bookingFormId": {
            "type": "string"
          },
          "promoCode": {
            "type": "string",
            "description": "A promotional offer's CODE, typed into the booking form's promo box.\n\nDECLARED HERE OR THE WHOLE BOOKING 400s. The global pipe runs with\n`forbidNonWhitelisted: true`, so an undeclared field is not silently\nstripped — it is rejected, and the public page already sends this on every\nsubmit where the box is non-empty. Omitting the declaration would take down\nevery public booking for every org, not merely the promotional ones.\n\nEvaluated the same way `bookingFormId` is: a typed code is tried first and,\nif it refuses, falls back to the link's own offer rather than costing the\ncustomer the reduction they already had. See\n`PromotionClaimService.previewForCart`."
          },
          "idempotencyKey": {
            "type": "string"
          },
          "purchaseCancellationInsurance": {
            "type": "boolean"
          },
          "cancellationInsurancePlanId": {
            "type": "string"
          },
          "schedulingContact": {
            "$ref": "#/components/schemas/SchedulingContactDto"
          }
        },
        "required": [
          "orgId",
          "contactName",
          "contactEmail",
          "contactPhone",
          "addressLine1"
        ]
      },
      "CheckoutQuoteDto": {
        "type": "object",
        "properties": {
          "orgId": {
            "type": "string",
            "description": "Provider (COMPANY) org the booking is placed against."
          },
          "packageId": {
            "type": "string"
          },
          "addOnIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "customAddOnIds": {
            "description": "Money Model v3 — Build-Your-Own picker selection (no packageId).",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "byoLayeredAddOnIds": {
            "description": "Ordinary add-ons layered on top of a BYO cart (charged at list price).",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "addOnQuantities": {
            "type": "object"
          },
          "selectedPackageVariantId": {
            "type": "string"
          },
          "selectedAddOnVariantIds": {
            "type": "object"
          },
          "addressLine1": {
            "type": "string",
            "description": "Street address of the property. Only used to GEOCODE when `lat`/`lng`\naren't supplied — the checkout paths (`createPublicOrder`,\n`createCheckoutSession`) run the address through `resolveAddress` and price\nthe service area off the geocoded point, so a quote that skipped that step\npriced against the base catalog while the charge priced against a service\narea with overridden package/insurance prices. Send the same address fields\nthe order submit sends and the two agree.\n\nOptional: omitting them (as the native iOS path does, which always has\ncoordinates) reproduces the previous behaviour exactly."
          },
          "addressLine2": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "countryCode": {
            "type": "string",
            "description": "Property country (ISO-2). Send it whenever the booking captured one: it\nstops a non-Canadian postcode being read as a Canadian FSA, so the quote\nand the charge agree for foreign addresses too."
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "scheduledTime": {
            "type": "string"
          },
          "schedulingMode": {
            "type": "string",
            "enum": [
              "scheduled",
              "requested"
            ]
          },
          "purchaseCancellationInsurance": {
            "type": "boolean"
          },
          "cancellationInsurancePlanId": {
            "type": "string"
          },
          "treatMissingTimeAsRequested": {
            "type": "boolean",
            "description": "When true, an order with no `scheduledTime` counts as REQUESTED scheduling\n— the rule the public booking path applies. The authenticated agent path\nuses `schedulingMode` verbatim, so it leaves this off."
          },
          "applyLoyaltyCredits": {
            "type": "boolean",
            "description": "Loyalty credits the customer wants spent on this order — the SAME two\nfields `CreateOrderDto` carries, so the quote can be asked the same\nquestion the checkout will be asked.\n\nThey were missing here, and that was the original hole: the checkout paths\nhave always deducted credits from the Stripe amount\n(`chargeableFromCheckoutTax(checkoutTax, creditsAppliedCents)`) while\n`grossTotalCents` — documented as \"exactly what the card will be charged\" —\ncould not even see them. A customer with credits was quoted one number and\ncharged a smaller one. Smaller is not harmless: the confirm screen is the\nonly place the reduction is visible, so a credit balance silently\nevaporated with nothing on screen saying it had been spent."
          },
          "loyaltyCreditsCents": {
            "type": "number"
          },
          "redeemEntitlementId": {
            "type": "string",
            "description": "LoyaltyEntitlement the customer intends to redeem on this order.\n\nQuoting NEVER reserves and never spends — this route creates nothing. It\nanswers one question: if this reward were applied to this exact cart, what\nwould the card be charged, and if it could NOT be applied, why not. The\n\"why not\" is the point: a `free_package` reward is 100% off the MATCHING\npackage's line, so a cart that does not contain that package cannot use it.\nReturning it as inapplicable-with-a-reason is the whole contract —\nsilently pricing it at zero would show the customer a reward they still\nhold as though they had just spent it.\n\nOwnership is checked against the CALLER. `POST /orders/quote` is `@Public()`\nso the anonymous booking portal can price a cart, and `JwtAuthGuard`\npopulates `req.user` best-effort there; with no caller the reward comes\nback `requires_sign_in` rather than being priced. The unauthenticated\nbooking flow cannot redeem, by design."
          },
          "bookingFormId": {
            "type": "string",
            "description": "The promotional booking link this cart is being priced through.\n\nTHE FIELD WHOSE ABSENCE WAS THE BUG. A booking form can carry an offer, and\nthe public page advertised it with \"Included with this booking. It is\napplied automatically, with nothing to enter\" — while this endpoint, which\nproduces the only total the customer ever sees, was never told which form\nthey had arrived through. It could not price the offer even in principle,\nso the review screen showed full list price under a banner promising the\nopposite, and the invoice then charged that full price.\n\nQuoting NEVER claims. The slot is taken at submission, so an abandoned cart\ncannot burn a claim out of a capped campaign."
          },
          "promoCode": {
            "type": "string",
            "description": "A coupon/offer code the customer typed on the review step.\n\nTakes precedence over the link's own offer — one booking settles at most\none incentive — but a code that turns out to be invalid does NOT cost them\nthe link's offer. See `PromotionClaimService.previewForCart`."
          },
          "contactEmail": {
            "type": "string",
            "description": "The address the customer has entered, when the flow has reached it.\n\nOffers can be `newCustomersOnly`, and whether this person is already a\ncustomer is not knowable on the client and must never be. The server\nanswers, and only about a person whose own address the caller supplied —\nthe same contract `GET /booking-forms/public/:id/promotion` already uses\nfor the banner. Without it an acquisition offer prices optimistically here\nand is then refused at submission, quoting a total that is never charged."
          }
        },
        "required": [
          "orgId"
        ]
      },
      "NewCustomerDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "phone": {
            "type": "string",
            "description": "REQUIRED. Creating a brand-new customer record inline on an order is a\nsignup — the provider ends up with a job to coordinate and needs a number\nto call. Enforced here on the backend, not just with a frontend asterisk.\n\nSafe to tighten in a single deploy: the field was ALREADY declared on this\nDTO, so a frontend that ships before the backend keeps working (it was\nalways allowed to send `phone`), and a frontend that ships after is already\ncollecting it. Adding a NEW field would have 400'd the whole request under\n`forbidNonWhitelisted` in one of the two orders — this does not.\n\nOnly bites when `newCustomer` is supplied at all; ordering for an EXISTING\ncustomer sends `customerId` and never touches this object, so no existing\nphone-less customer is blocked from having an order placed for them."
          },
          "notes": {
            "type": "string"
          }
        },
        "required": [
          "name",
          "phone"
        ]
      },
      "CreateOrderDto": {
        "type": "object",
        "properties": {
          "providerOrgId": {
            "type": "string"
          },
          "customerId": {
            "type": "string"
          },
          "newCustomer": {
            "$ref": "#/components/schemas/NewCustomerDto"
          },
          "addressLine1": {
            "type": "string"
          },
          "addressLine2": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "scheduledTime": {
            "type": "string"
          },
          "estimatedDuration": {
            "type": "number"
          },
          "schedulingMode": {
            "enum": [
              "scheduled",
              "requested"
            ],
            "type": "string"
          },
          "mediaTypes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "priority": {
            "enum": [
              "standard",
              "rush",
              "urgent"
            ],
            "type": "string"
          },
          "notes": {
            "type": "string"
          },
          "technicianId": {
            "type": "string"
          },
          "editorId": {
            "type": "string"
          },
          "projectManagerId": {
            "type": "string"
          },
          "packageId": {
            "type": "string"
          },
          "addOnIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "customAddOnIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "addOnQuantities": {
            "type": "object"
          },
          "requiredKitId": {
            "type": "string",
            "nullable": true
          },
          "selectedPackageVariantId": {
            "type": "string"
          },
          "selectedAddOnVariantIds": {
            "type": "object"
          },
          "bookingAnswers": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "idempotencyKey": {
            "type": "string"
          },
          "applyLoyaltyCredits": {
            "type": "boolean"
          },
          "loyaltyCreditsCents": {
            "type": "number"
          },
          "redeemEntitlementId": {
            "type": "string",
            "description": "LoyaltyEntitlement the customer is spending on THIS order — the reward\nthey were shown on the confirm screen. Single use: the checkout paths hold\nit (`pending` → `reserved`) before the card is touched and fulfilment\nburns it (`reserved` → `redeemed`), both as conditional UPDATEs guarded on\nthe current status.\n\nIt has to be a field on THIS class and not an inline `@Body()` literal:\n`ValidationPipe` runs `forbidNonWhitelisted: true`, so an id sent against a\nDTO that does not declare it is a 400 rather than a silently ignored\nreward — and a route whose body has no DTO class at all is skipped by the\npipe entirely, which on a route that hands out free packages would be free\ninventory.\n\nThe SERVER decides what it is worth. Nothing about the reduction is taken\nfrom the client: `resolveRedeemableReward` re-reads the entitlement, checks\nit belongs to this customer in this org, and prices it against the cart it\nwas actually applied to."
          },
          "discountId": {
            "type": "string",
            "description": "An `OrgDiscount` the STAFF member booking this job is attaching to it —\nby row id (a named markdown the org configured) or by coupon `code`.\nSend exactly one; both, or a blank, is refused as `ambiguous_request`.\n\n── WHAT THE CLIENT DOES NOT GET TO DECIDE ─────────────────────────────────\nNeither field carries an AMOUNT, deliberately. The server re-reads the\n`OrgDiscount` row in the PROVIDER's org, re-prices it against the invoice\nit actually built, and clamps it — see `priceCoupon`. A client that could\nname its own reduction would be a client that could make any order free.\n\n── AND WHO GETS TO SEND THEM ──────────────────────────────────────────────\n`POST /orders/create` is reachable by customers, so declaring these on the\nDTO does NOT authorize them: `BookingIncentivesService.assertMayApplyIncentives`\nchecks `invoices.discount:write` IN THE PROVIDER ORG before either is\nhonoured. They are declared here because `ValidationPipe` runs\n`forbidNonWhitelisted: true` — an undeclared field is a 400, which would\nmake a coordinator's coupon look like a broken booking.\n\n── AND WHICH ROUTES CAN ACTUALLY SETTLE ONE ───────────────────────────────\nONLY the doors that raise an INVOICE: `POST /orders/create` on its\ninvoice-after-delivery branch, and the staff New Project door. A coupon is\na flat post-tax snapshot off a taxed invoice, and the card-at-checkout\nroutes (`POST /orders/checkout`, `POST /orders/native-checkout`, and\n`POST /orders/create` when the provider org requires upfront payment)\ncharge before any invoice exists. Those routes REFUSE these two fields\nwith a 400 rather than ignoring them — see\n`OrdersService.assertCheckoutCannotCarryCoupon`. `redeemEntitlementId`\nabove is different and IS honoured there, because a reward reduces the\ncart's taxable base before the charge."
          },
          "discountCode": {
            "type": "string"
          },
          "purchaseCancellationInsurance": {
            "type": "boolean"
          },
          "cancellationInsurancePlanId": {
            "type": "string"
          },
          "schedulingContact": {
            "$ref": "#/components/schemas/SchedulingContactDto"
          }
        },
        "required": [
          "addressLine1",
          "mediaTypes",
          "priority"
        ]
      },
      "ConfirmCancelCardDto": {
        "type": "object",
        "properties": {
          "paymentMethodId": {
            "type": "string"
          }
        },
        "required": [
          "paymentMethodId"
        ]
      },
      "AcceptCancellationPolicyDto": {
        "type": "object",
        "properties": {
          "setupIntentClientSecret": {
            "type": "string"
          }
        },
        "required": [
          "setupIntentClientSecret"
        ]
      },
      "CommentDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "content": {
            "type": "string"
          },
          "timestamp": {
            "format": "date-time",
            "type": "string"
          },
          "user": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "avatarUrl": {
                "type": "string",
                "nullable": true
              }
            },
            "required": [
              "id",
              "name"
            ]
          }
        },
        "required": [
          "id",
          "content",
          "timestamp",
          "user"
        ]
      },
      "DownloadAllDto": {
        "type": "object",
        "properties": {
          "mediaTypes": {
            "type": "array",
            "description": "Filter download by media types",
            "items": {
              "type": "string",
              "enum": [
                "PHOTO",
                "VIDEO",
                "FLOORPLAN",
                "DOCUMENT",
                "VIRTUAL_TOUR",
                "PROPERTY_WEBSITE",
                "BROCHURE"
              ]
            }
          },
          "quality": {
            "type": "string",
            "enum": [
              "print",
              "web"
            ],
            "description": "Download quality tier. 'print' (default) zips original/print-quality files; 'web' zips the web/MLS-quality variant (mls → webReady → original fallback).",
            "default": "print"
          }
        }
      },
      "RequestChangesDto": {
        "type": "object",
        "properties": {
          "feedback": {
            "type": "string",
            "description": "Feedback text for requested changes"
          }
        },
        "required": [
          "feedback"
        ]
      },
      "AddCommentDto": {
        "type": "object",
        "properties": {
          "content": {
            "type": "string",
            "description": "Comment content"
          }
        },
        "required": [
          "content"
        ]
      },
      "ProjectRatingItemDto": {
        "type": "object",
        "properties": {
          "role": {
            "type": "string",
            "description": "Role being rated: TECHNICIAN, EDITOR, PROJECT_MANAGER, or COMPANY"
          },
          "rateeId": {
            "type": "string",
            "description": "User ID of the person being rated (null for COMPANY)"
          },
          "orgId": {
            "type": "string",
            "description": "Organization ID (set when rating the company)"
          },
          "score": {
            "type": "number",
            "minimum": 1,
            "maximum": 5,
            "description": "Rating score from 1 to 5"
          },
          "comment": {
            "type": "string",
            "description": "Optional comment"
          }
        },
        "required": [
          "role",
          "score"
        ]
      },
      "CreateProjectRatingsDto": {
        "type": "object",
        "properties": {
          "ratings": {
            "description": "Array of ratings for team members and/or company",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectRatingItemDto"
            }
          }
        },
        "required": [
          "ratings"
        ]
      },
      "SurveyAnswerDto": {
        "type": "object",
        "properties": {
          "questionId": {
            "type": "string"
          },
          "value": {
            "type": "object"
          }
        },
        "required": [
          "questionId",
          "value"
        ]
      },
      "SubmitSurveyResponseDto": {
        "type": "object",
        "properties": {
          "surveyId": {
            "type": "string"
          },
          "skipped": {
            "type": "boolean"
          },
          "answers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SurveyAnswerDto"
            }
          }
        },
        "required": [
          "surveyId"
        ]
      },
      "MediaDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "key": {
            "type": "string",
            "nullable": true
          },
          "cdnUrl": {
            "type": "string",
            "nullable": true
          },
          "externalUrl": {
            "type": "string",
            "nullable": true
          },
          "signedUrl": {
            "type": "string",
            "nullable": true
          },
          "signedThumbnailUrl": {
            "type": "string",
            "nullable": true
          },
          "thumbnailUrl": {
            "type": "string",
            "nullable": true
          },
          "printDownloadUrl": {
            "type": "string",
            "nullable": true
          },
          "webDownloadUrl": {
            "type": "string",
            "nullable": true,
            "description": "Signed, ready-to-use download link for the WEB / MLS quality variant\n(2048px mls → 1920px webReady → original fallback). Forces an attachment\nsave. Null only when the media is not an image / has no original at all."
          },
          "webReadyCdnUrl": {
            "type": "string",
            "nullable": true,
            "description": "Direct 1920px web-ready CDN URL (no redirect, no expiry), if generated."
          },
          "embedUrl": {
            "type": "string",
            "nullable": true,
            "description": "Absolute, zero-chrome embeddable player URL for the download-centre iframe\nsnippet (`/embed/video/:token/:mediaId`). VIDEO rows only; null for every\nother media type and null when paywalled (an embed would stream the file\npast the paywall). Points at the org's white-label host when one is\nverified, else the platform frontend origin."
          },
          "filename": {
            "type": "string"
          },
          "size": {
            "type": "number"
          },
          "type": {
            "type": "object"
          },
          "createdAt": {
            "format": "date-time",
            "type": "string"
          }
        },
        "required": [
          "id",
          "key",
          "cdnUrl",
          "externalUrl",
          "filename",
          "size",
          "type",
          "createdAt"
        ]
      },
      "DeliveryResponseDto": {
        "type": "object",
        "properties": {
          "project": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "addressLine1": {
                "type": "string",
                "nullable": true
              },
              "addressLine2": {
                "type": "string",
                "nullable": true
              },
              "city": {
                "type": "string",
                "nullable": true
              },
              "region": {
                "type": "string",
                "nullable": true
              },
              "scheduledTime": {
                "format": "date-time",
                "type": "string",
                "nullable": true
              },
              "status": {
                "type": "object"
              },
              "clientApprovalStatus": {
                "type": "object"
              },
              "clientApprovedAt": {
                "format": "date-time",
                "type": "string",
                "nullable": true
              },
              "deliveryEnabledAt": {
                "format": "date-time",
                "type": "string",
                "nullable": true
              },
              "clientDeliveryNote": {
                "type": "string",
                "nullable": true
              }
            },
            "required": [
              "id",
              "addressLine1",
              "addressLine2",
              "city",
              "region",
              "scheduledTime",
              "status",
              "clientApprovalStatus",
              "clientApprovedAt",
              "deliveryEnabledAt",
              "clientDeliveryNote"
            ]
          },
          "organization": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "logoUrl": {
                "type": "string",
                "nullable": true
              },
              "brandColor": {
                "type": "string",
                "nullable": true
              },
              "supportEmail": {
                "type": "string",
                "nullable": true
              },
              "primaryEmail": {
                "type": "string",
                "nullable": true
              },
              "phone": {
                "type": "string",
                "nullable": true
              }
            },
            "required": [
              "id",
              "name",
              "logoUrl",
              "brandColor",
              "supportEmail",
              "primaryEmail",
              "phone"
            ]
          },
          "media": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MediaDto"
            }
          },
          "pendingDeliverables": {
            "description": "Ordered deliverable types the client purchased that have NOT been released\nto them yet — the \"— in production\" chips on the download centre. Values are\nMediaType strings, preserving the project's ordered sequence.\n\nDerived on the RELEASE basis once the order has any `ProjectDeliveryRelease`\nrow (ordered types minus the union of every send's `mediaTypes`), which is\nthe same basis the partial-delivery email's \"Still in production:\" list uses\n— the two are shown to the same person about the same order and must agree.\n\nFor an order with no release rows (everything delivered before partial\ndelivery existed) it falls back to the original derivation: ordered types\nminus the media types actually present, with PROPERTY_WEBSITE and a\ndeliberately-unpublished VIRTUAL_TOUR excluded.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "comments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CommentDto"
            }
          },
          "customer": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string",
                "nullable": true
              }
            },
            "required": [
              "id",
              "name",
              "email"
            ]
          },
          "canApprove": {
            "type": "boolean"
          },
          "awaitingFinalDelivery": {
            "type": "boolean",
            "description": "True when the most recent send released only PART of the order, so the\nclient is looking at a delivery that is not finished.\n\nApproval is what flips the project to DELIVERED and publishes the property\nwebsite, so it must not be offered here — `approveDelivery` refuses it, and\nthis flag is what stops the page rendering a button that 400s. Separate from\n`canApprove` on purpose: the reader is still the approver and still gets\n\"Request changes\", they simply cannot sign off yet."
          },
          "canComment": {
            "type": "boolean"
          },
          "viewerAuthenticated": {
            "type": "boolean",
            "description": "Whether THIS request carried a session the API accepted — i.e. whether the\nflags above were computed for a signed-in person or for an anonymous\nlink-holder.\n\nExists because the two are indistinguishable from the client otherwise, and\nthey need opposite handling. `GET /delivery/:token` is `@Public()` with\noptional auth: a stale or expired bearer token is silently ignored (no 401,\nso nothing triggers the client's refresh) and the page renders an anonymous\npayload while the app shell still shows the user signed in. Read as \"the\ncustomer is not recognised\", which is what put \"Only the customer can add\ncomments.\" in front of actual customers.\n\nThe client compares it against its own session: signed in locally + false\nhere = the credential didn't travel ⇒ refresh and refetch, don't tell the\nreader they're the wrong person."
          },
          "canViewDiscussion": {
            "type": "boolean",
            "description": "Whether the viewer may see the customer discussion at all. True only for\norg team members and the linked customer — never an anonymous holder of the\npublic delivery link. Gates both the comments payload and the discussion UI."
          },
          "deliveryChatCutoffAt": {
            "format": "date-time",
            "type": "string",
            "nullable": true,
            "description": "When the shown discussion starts — the instant the project was FIRST\ndelivered. Messages before it (the pre-delivery scheduling/ops thread) are\nfiltered out SERVER-SIDE, so they never reach this public page. Null when no\ndelivery instant could be resolved (legacy/imported projects): the whole\nthread is shown and the UI renders no cutoff marker. NOT interchangeable\nwith project.deliveryEnabledAt, which is the LATEST delivery and moves on\nevery re-delivery."
          },
          "downloadEnabled": {
            "type": "boolean",
            "description": "Whether bulk download is available (requires storage backend)"
          },
          "maxDownloadFiles": {
            "type": "number",
            "description": "Server cap on files per bulk ZIP (MAX_MEDIA_FILES_PER_ZIP). Lets the\n download dialog warn before a request that would 400 with TOO_MANY_FILES."
          },
          "canRetryArtifact": {
            "type": "boolean",
            "description": "Whether user can retry failed artifacts (OWNER/ADMIN/PROJECT_MANAGER only)"
          },
          "isStaffViewer": {
            "type": "boolean",
            "description": "True when the viewer is the SERVICE PROVIDER's side of this delivery, not\nthe client. Gates staff-perspective copy — a preview banner, an\n\"awaiting the customer\" note — which otherwise renders to the customer and\ntalks about them in the third person on their own delivery page."
          },
          "canDownloadUnpaid": {
            "type": "boolean",
            "description": "True only when the delivery IS paywalled (a blocking invoice is outstanding)\nAND the viewer is staff of the organisation that owns the project — an\nelevated OrganizationMember of `project.orgId`, per\n`PaywallService.isOwningOrgStaff`.\n\nStaff may pull unpaid deliverables; customers may not, and neither may the\nlinked agent-customer who is PERSONAL_OWNER of their own organisation. When\nthis is true the download centre keeps its download affordances on screen\nand takes their URLs from the AUTHENTICATED `/delivery/:token/staff/*`\nroutes — every URL in `media` here still points at the public token routes,\nwhich paywall unconditionally.\n\nFalse for everyone on an unpaywalled delivery (nothing to be exempt from)."
          },
          "team": {
            "type": "object",
            "properties": {
              "technician": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  }
                },
                "selfRequired": false
              },
              "editor": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  }
                },
                "selfRequired": false
              },
              "projectManager": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string"
                  }
                },
                "selfRequired": false
              }
            },
            "required": [
              "technician",
              "editor",
              "projectManager"
            ]
          },
          "hasRated": {
            "type": "boolean",
            "description": "Whether the customer has already rated this project"
          },
          "capture": {
            "type": "object",
            "properties": {
              "propertyId": {
                "type": "string"
              },
              "status": {
                "type": "string"
              },
              "totalSqFt": {
                "type": "number",
                "nullable": true
              },
              "floorCount": {
                "type": "number"
              },
              "roomCount": {
                "type": "number"
              },
              "hasTour": {
                "type": "boolean"
              },
              "tourUrl": {
                "type": "string",
                "nullable": true
              },
              "hasFloorplan": {
                "type": "boolean"
              },
              "floorplanUrl": {
                "type": "string",
                "nullable": true
              }
            },
            "required": [
              "propertyId",
              "status",
              "totalSqFt",
              "floorCount",
              "roomCount",
              "hasTour",
              "tourUrl",
              "hasFloorplan",
              "floorplanUrl"
            ]
          },
          "listingUrl": {
            "type": "string",
            "nullable": true,
            "description": "Property website listing URL (if published)"
          },
          "unbrandedListingUrl": {
            "type": "string",
            "nullable": true,
            "description": "MLS-safe copy of `listingUrl` at its OWN address —\n`/listing/<slug>/unbranded`, a distinct route rather than a flag on the\nbranded one, because the MLS data model holds two separate URLs\n(`VirtualTourURLBranded` / `VirtualTourURLUnbranded`) and boards judge the\ndestination the link resolves to. It renders from a payload that never\ncarried the org, the agent or their contact details: no team or contact\nsection, no \"Presented by\" eyebrow, no footer, no provider name in the\ntitle/OG/JSON-LD, and no company favicon. Safe to paste into an MLS\n\"Virtual Tour\" field. Null whenever `listingUrl` is null."
          },
          "invoice": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "status": {
                "type": "string"
              },
              "paymentToken": {
                "type": "string",
                "nullable": true
              },
              "total": {
                "type": "number"
              },
              "currency": {
                "type": "string"
              },
              "isBlocking": {
                "type": "boolean"
              }
            },
            "required": [
              "id",
              "status",
              "paymentToken",
              "total",
              "currency",
              "isBlocking"
            ]
          }
        },
        "required": [
          "project",
          "organization",
          "media",
          "pendingDeliverables",
          "comments",
          "canApprove",
          "awaitingFinalDelivery",
          "canComment",
          "viewerAuthenticated",
          "canViewDiscussion",
          "downloadEnabled",
          "maxDownloadFiles",
          "canRetryArtifact",
          "isStaffViewer",
          "canDownloadUnpaid"
        ]
      },
      "RetryArtifactDto": {
        "type": "object",
        "properties": {
          "artifactId": {
            "type": "string",
            "description": "ID of the artifact to retry"
          }
        },
        "required": [
          "artifactId"
        ]
      },
      "SurveyQuestionDto": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "config": {
            "type": "object"
          },
          "role": {
            "type": "string"
          },
          "scoring": {
            "type": "object"
          },
          "required": {
            "type": "boolean"
          },
          "sortOrder": {
            "type": "number",
            "minimum": 0
          }
        },
        "required": [
          "title",
          "type"
        ]
      },
      "CreateSurveyDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "isActive": {
            "type": "boolean"
          },
          "isDefault": {
            "type": "boolean"
          },
          "questions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SurveyQuestionDto"
            }
          }
        },
        "required": [
          "name"
        ]
      },
      "UpdateSurveyQuestionDto": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "config": {
            "type": "object"
          },
          "role": {
            "type": "string"
          },
          "scoring": {
            "type": "object"
          },
          "required": {
            "type": "boolean"
          },
          "sortOrder": {
            "type": "number",
            "minimum": 0
          },
          "id": {
            "type": "string"
          }
        },
        "required": [
          "title",
          "type"
        ]
      },
      "UpdateSurveyDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "isActive": {
            "type": "boolean"
          },
          "isDefault": {
            "type": "boolean"
          },
          "displayOrder": {
            "type": "number",
            "minimum": 0
          },
          "questions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UpdateSurveyQuestionDto"
            }
          }
        }
      },
      "CreateReviewPlatformDto": {
        "type": "object",
        "properties": {
          "platform": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          }
        },
        "required": [
          "platform",
          "name",
          "url"
        ]
      },
      "UpdateReviewPlatformDto": {
        "type": "object",
        "properties": {
          "platform": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string"
          },
          "enabled": {
            "type": "boolean"
          },
          "sortOrder": {
            "type": "number",
            "minimum": 0
          }
        }
      },
      "UpdateTourProgressDto": {
        "type": "object",
        "properties": {
          "tourTrack": {
            "type": "string"
          },
          "stepId": {
            "type": "string"
          },
          "completed": {
            "type": "boolean"
          },
          "skipped": {
            "type": "boolean"
          }
        },
        "required": [
          "tourTrack",
          "stepId"
        ]
      },
      "CreateOrgDiscountDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 160,
            "description": "Display name (e.g. \"Friends & Family\")"
          },
          "code": {
            "type": "string",
            "maxLength": 64,
            "description": "Coupon code. Present => this is a coupon (uppercased on write, unique per org). Omit / null => a named discount preset."
          },
          "type": {
            "type": "string",
            "enum": [
              "PERCENT",
              "FIXED"
            ],
            "description": "PERCENT or FIXED"
          },
          "value": {
            "type": "number",
            "minimum": 0,
            "description": "PERCENT: whole percent 0..100. FIXED: amount in cents."
          },
          "description": {
            "type": "string",
            "maxLength": 500,
            "description": "Internal description / notes"
          },
          "active": {
            "type": "boolean",
            "description": "Whether the discount is active (default true)"
          },
          "offerKind": {
            "type": "string",
            "enum": [
              "free_items",
              "discount_next_order_pct",
              "discount_next_order_cents"
            ],
            "description": "Makes this a claimable offer rather than an invoice discount. One of: free_items, discount_next_order_pct, discount_next_order_cents"
          },
          "offerPayload": {
            "type": "object",
            "description": "e.g. { \"packageId\": \"...\" }"
          },
          "newCustomersOnly": {
            "type": "boolean",
            "description": "Acquisition-only when true."
          },
          "startsAt": {
            "type": "string"
          },
          "endsAt": {
            "type": "string"
          },
          "maxClaims": {
            "type": "number",
            "minimum": 1,
            "description": "Total claims allowed. Omit for unlimited."
          }
        },
        "required": [
          "name",
          "type",
          "value"
        ]
      },
      "UpdateOrgDiscountDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 160,
            "description": "Display name"
          },
          "code": {
            "type": "string",
            "maxLength": 64,
            "description": "Coupon code (uppercased on write, unique per org). Pass empty string / null to clear the code and make it a named discount."
          },
          "type": {
            "type": "string",
            "enum": [
              "PERCENT",
              "FIXED"
            ],
            "description": "PERCENT or FIXED"
          },
          "value": {
            "type": "number",
            "minimum": 0,
            "description": "PERCENT: whole percent 0..100. FIXED: amount in cents."
          },
          "description": {
            "type": "string",
            "maxLength": 500,
            "description": "Internal description / notes"
          },
          "active": {
            "type": "boolean",
            "description": "Whether the discount is active"
          },
          "offerKind": {
            "type": "string",
            "enum": [
              "free_items",
              "discount_next_order_pct",
              "discount_next_order_cents"
            ],
            "description": "Makes this a claimable offer rather than an invoice discount. One of: free_items, discount_next_order_pct, discount_next_order_cents"
          },
          "offerPayload": {
            "type": "object",
            "description": "e.g. { \"packageId\": \"...\" }"
          },
          "newCustomersOnly": {
            "type": "boolean",
            "description": "Acquisition-only when true."
          },
          "startsAt": {
            "type": "string"
          },
          "endsAt": {
            "type": "string"
          },
          "maxClaims": {
            "type": "number",
            "minimum": 1,
            "description": "Total claims allowed. Omit for unlimited."
          }
        }
      },
      "CreateSlotDto": {
        "type": "object",
        "properties": {
          "type": {
            "type": "object"
          },
          "typeOther": {
            "type": "string",
            "description": "The agent's own words for an OTHER slot. 120 chars because every surface\nthat renders it (job feed card, detail panel, listing row, active-job card)\nshows it on one line in place of a type label.",
            "maxLength": 120
          },
          "turnaroundHours": {
            "type": "number",
            "description": "Hours to deliver, from assignment. Overrides the job's default for this\none service — photos in 24h next to a floor plan in 3 days is the normal\ncase, not an edge case.",
            "minimum": 1
          },
          "budgetMinCents": {
            "type": "number",
            "minimum": 0
          },
          "budgetMaxCents": {
            "type": "number",
            "minimum": 0
          }
        },
        "required": [
          "type"
        ]
      },
      "CreateMarketplaceJobDto": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 160
          },
          "description": {
            "type": "string",
            "maxLength": 4000
          },
          "packageName": {
            "type": "string",
            "maxLength": 200
          },
          "requestedDate": {
            "type": "string"
          },
          "budgetMinCents": {
            "type": "number",
            "minimum": 0
          },
          "budgetMaxCents": {
            "type": "number",
            "minimum": 0
          },
          "currency": {
            "type": "string",
            "maxLength": 12
          },
          "defaultTurnaroundHours": {
            "type": "number",
            "description": "The turnaround for the whole job, copied onto every slot that does not\noverride it. Optional: an agent with no strong view leaves it unset and\nthe work is simply unmeasured, which is honest. A required field on the\nwizard's longest step gets a guess typed into it, and a wrong deadline is\nworse than none.",
            "minimum": 1
          },
          "autoAssignEnabled": {
            "type": "boolean"
          },
          "paymentStructure": {
            "type": "object"
          },
          "deliveryMilestonePct": {
            "type": "number",
            "minimum": 1
          },
          "revisionLimit": {
            "type": "number",
            "minimum": 0
          },
          "slots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreateSlotDto"
            }
          },
          "locationAddressLine1": {
            "type": "string",
            "maxLength": 200
          },
          "locationAddressLine2": {
            "type": "string",
            "maxLength": 200
          },
          "locationCity": {
            "type": "string",
            "maxLength": 120
          },
          "locationRegion": {
            "type": "string",
            "maxLength": 120
          },
          "locationPostalCode": {
            "type": "string",
            "maxLength": 32
          },
          "locationCountryCode": {
            "type": "string",
            "maxLength": 8
          },
          "locationLat": {
            "type": "number"
          },
          "locationLng": {
            "type": "number"
          },
          "targetedProviderIds": {
            "description": "Optional list of provider userIds to target with this job. When set,\nonly the listed providers (and the job's creator) can see and bid on\nthe job — public marketplace browsing skips it. An empty list means\nthe job is open to the whole marketplace.",
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "title",
          "requestedDate",
          "slots"
        ]
      },
      "CreateMarketplaceBidDto": {
        "type": "object",
        "properties": {
          "amountCents": {
            "type": "number",
            "minimum": 0
          },
          "message": {
            "type": "string",
            "maxLength": 2000
          },
          "distanceMiles": {
            "type": "number",
            "minimum": 0
          },
          "etaHours": {
            "type": "number",
            "minimum": 0
          }
        }
      },
      "SendMarketplaceMessageDto": {
        "type": "object",
        "properties": {
          "body": {
            "type": "string",
            "description": "Trimmed before validation, so a body of spaces is caught as empty rather\nthan stored as a blank bubble.",
            "maxLength": 4000
          }
        },
        "required": [
          "body"
        ]
      },
      "CounterMarketplaceBidDto": {
        "type": "object",
        "properties": {
          "amountCents": {
            "type": "number",
            "description": "The proposed price, in cents. Required and positive: a negotiation that\ncan reach zero by accident is one that can assign a slot nobody gets paid\nfor, and a $0 authorization fails downstream anyway.",
            "minimum": 1
          },
          "message": {
            "type": "string",
            "description": "Why. Optional — a number on its own is a valid counter.",
            "maxLength": 1000
          }
        },
        "required": [
          "amountCents"
        ]
      },
      "DeclineMarketplaceBookingConfirmationDto": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "maxLength": 500
          }
        }
      },
      "CreateMarketplaceRatingDto": {
        "type": "object",
        "properties": {
          "rateeId": {
            "type": "string",
            "format": "uuid"
          },
          "score": {
            "type": "number",
            "minimum": 1,
            "maximum": 5
          },
          "comment": {
            "type": "string",
            "maxLength": 1000
          }
        },
        "required": [
          "rateeId",
          "score"
        ]
      },
      "CancelMarketplaceSlotDto": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "maxLength": 500
          }
        }
      },
      "AuthorizeEscrowDto": {
        "type": "object",
        "properties": {
          "amountCents": {
            "type": "number",
            "minimum": 1
          },
          "currency": {
            "type": "string",
            "maxLength": 12
          },
          "connectedAccountId": {
            "type": "string"
          }
        }
      },
      "UpsertProviderProfileDto": {
        "type": "object",
        "properties": {
          "headline": {
            "type": "string",
            "maxLength": 200
          },
          "bio": {
            "type": "string",
            "maxLength": 4000
          },
          "specialties": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "equipment": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "certifications": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "portfolioUrls": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "documents": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "serviceArea": {
            "type": "string",
            "description": "Where the provider will travel to, in their own words — \"Calgary and\nsurrounding areas\", \"Greater Vancouver, will travel for full-day shoots\".\n\nSELF-DECLARED, and that is fine here in a way it is not for\n`insuranceVerified`. This is a statement of preference the provider is\nentitled to make about themselves; insurance is a claim that a human\nreviewed a document, which a checkbox cannot satisfy. That is why one of\nthese ships and the other stays \"Coming soon\".\n\nFree text rather than a picker because a provider's real coverage is a\nsentence with conditions in it, and a dropdown of cities would force them\nto pick the nearest lie.",
            "maxLength": 200
          },
          "isAvailable": {
            "type": "boolean"
          },
          "backgroundCheckStatus": {
            "type": "object"
          }
        }
      },
      "CreateApiKeyDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "expiresAt": {
            "type": "string"
          },
          "rateLimitPerMin": {
            "type": "number",
            "minimum": 10,
            "maximum": 1000
          }
        },
        "required": [
          "name",
          "scopes"
        ]
      },
      "BulkImportCustomerItem": {
        "type": "object",
        "properties": {
          "externalId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "phone": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          }
        },
        "required": [
          "name"
        ]
      },
      "BulkImportCustomersDto": {
        "type": "object",
        "properties": {
          "items": {
            "maxItems": 500,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BulkImportCustomerItem"
            }
          }
        },
        "required": [
          "items"
        ]
      },
      "BulkImportProjectItem": {
        "type": "object",
        "properties": {
          "externalId": {
            "type": "string"
          },
          "customerExternalId": {
            "type": "string"
          },
          "customerId": {
            "type": "string"
          },
          "technicianId": {
            "type": "string"
          },
          "scheduledTime": {
            "type": "string"
          },
          "orderedAt": {
            "type": "string"
          },
          "paidAt": {
            "type": "string"
          },
          "status": {
            "type": "object"
          },
          "addressLine1": {
            "type": "string"
          },
          "addressLine2": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "notes": {
            "type": "string"
          },
          "paymentAmount": {
            "type": "number"
          },
          "paymentCurrency": {
            "type": "string"
          },
          "packageId": {
            "type": "string"
          },
          "selectedAddOnIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "mediaTypes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "importSource": {
            "type": "string"
          }
        },
        "required": [
          "scheduledTime"
        ]
      },
      "BulkImportProjectsDto": {
        "type": "object",
        "properties": {
          "items": {
            "maxItems": 200,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BulkImportProjectItem"
            }
          }
        },
        "required": [
          "items"
        ]
      },
      "BulkImportInvoiceLineItem": {
        "type": "object",
        "properties": {
          "description": {
            "type": "string"
          },
          "quantity": {
            "type": "number"
          },
          "unitPrice": {
            "type": "number"
          },
          "total": {
            "type": "number"
          }
        },
        "required": [
          "description",
          "quantity",
          "unitPrice"
        ]
      },
      "BulkImportInvoiceItem": {
        "type": "object",
        "properties": {
          "externalId": {
            "type": "string"
          },
          "customerExternalId": {
            "type": "string"
          },
          "customerId": {
            "type": "string"
          },
          "projectExternalId": {
            "type": "string"
          },
          "projectId": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BulkImportInvoiceLineItem"
            }
          },
          "taxRate": {
            "type": "number"
          },
          "currency": {
            "type": "string"
          },
          "status": {
            "type": "object"
          },
          "dueDate": {
            "type": "string"
          },
          "notes": {
            "type": "string"
          }
        },
        "required": [
          "items"
        ]
      },
      "BulkImportInvoicesDto": {
        "type": "object",
        "properties": {
          "items": {
            "maxItems": 200,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BulkImportInvoiceItem"
            }
          }
        },
        "required": [
          "items"
        ]
      },
      "AryeoTestConnectionDto": {
        "type": "object",
        "properties": {
          "apiKey": {
            "type": "string"
          }
        },
        "required": [
          "apiKey"
        ]
      },
      "AryeoStartMigrationDto": {
        "type": "object",
        "properties": {
          "apiKey": {
            "type": "string"
          },
          "entities": {
            "type": "array",
            "minItems": 1,
            "maxItems": 6,
            "items": {
              "type": "string",
              "enum": [
                "orders",
                "customers",
                "listings",
                "team_members",
                "products",
                "order_forms"
              ]
            }
          },
          "dateFrom": {
            "type": "string"
          },
          "dateTo": {
            "type": "string"
          },
          "autoImportPackages": {
            "type": "boolean"
          }
        },
        "required": [
          "entities"
        ]
      },
      "UpsertScanProjectBackupDto": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1
          },
          "payload": {
            "type": "object"
          },
          "panoramaKeys": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "schemaVersion": {
            "type": "number"
          },
          "clientCreatedAt": {
            "type": "string"
          },
          "clientUpdatedAt": {
            "type": "string"
          }
        },
        "required": [
          "title",
          "payload",
          "schemaVersion",
          "clientCreatedAt",
          "clientUpdatedAt"
        ]
      },
      "PresignBackupPanoDto": {
        "type": "object",
        "properties": {
          "filename": {
            "type": "string",
            "description": "The pano file name, e.g. \"<spotId>.jpg\". Used as the deterministic key."
          },
          "contentType": {
            "type": "string"
          }
        },
        "required": [
          "filename",
          "contentType"
        ]
      },
      "UploadL2PackageDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1
          },
          "projectId": {
            "type": "string"
          },
          "source": {
            "type": "object",
            "enum": [
              "L2_DIRECT",
              "ROBOT_DOG"
            ]
          },
          "deviceLabel": {
            "type": "string",
            "minLength": 1
          },
          "runId": {
            "type": "string",
            "minLength": 1
          },
          "capturedAt": {
            "type": "string"
          },
          "firmwareVersion": {
            "type": "string"
          },
          "coverageNote": {
            "type": "string"
          },
          "schemaVersion": {
            "type": "number"
          },
          "canvas": {
            "type": "object"
          }
        },
        "required": [
          "id",
          "source",
          "deviceLabel",
          "runId",
          "capturedAt",
          "schemaVersion",
          "canvas"
        ]
      },
      "ReportVisitDurationDto": {
        "type": "object",
        "properties": {
          "visitId": {
            "type": "string",
            "description": "The id returned by the surface's own view route (`/listings/:x/view` or\n`/tours/:id/view`). One table serves both surfaces, so one endpoint\nreports duration for both.",
            "maxLength": 64
          },
          "seconds": {
            "type": "number",
            "description": "Seconds on the page. Integer — the column is an Int, and a float would\nthrow at the database rather than at the edge.\n\nZERO IS VALID AND MEANINGFUL: it is a real bounce, and it is not the same\nthing as never reporting (which leaves the column null). `@Min(0)`, not\n`@Min(1)`, for exactly that reason.",
            "minimum": 0,
            "maximum": 14400
          }
        },
        "required": [
          "visitId",
          "seconds"
        ]
      },
      "CreateListingLeadDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200
          },
          "email": {
            "type": "string",
            "maxLength": 320,
            "format": "email"
          },
          "phone": {
            "type": "string",
            "maxLength": 50
          },
          "message": {
            "type": "string",
            "maxLength": 5000
          },
          "visitorId": {
            "type": "string",
            "maxLength": 64
          },
          "eventId": {
            "type": "string",
            "maxLength": 100
          },
          "referrer": {
            "type": "string",
            "maxLength": 1000
          },
          "utmSource": {
            "type": "string",
            "maxLength": 200
          },
          "utmMedium": {
            "type": "string",
            "maxLength": 200
          },
          "utmCampaign": {
            "type": "string",
            "maxLength": 200
          },
          "utmTerm": {
            "type": "string",
            "maxLength": 200
          },
          "utmContent": {
            "type": "string",
            "maxLength": 200
          },
          "fbclid": {
            "type": "string",
            "maxLength": 400
          },
          "gclid": {
            "type": "string",
            "maxLength": 400
          },
          "ttclid": {
            "type": "string",
            "maxLength": 400
          },
          "msclkid": {
            "type": "string",
            "maxLength": 400
          },
          "fbc": {
            "type": "string",
            "description": "Meta's OWN cookies, read verbatim from `document.cookie`.\n\n`_fbc` is the click cookie and `_fbp` the browser cookie. When the browser\ncan hand them over they beat anything we derive, because they are the exact\nvalues Meta itself wrote; `fbc` in particular can only otherwise be\nreconstructed as `fb.1.<click time>.<fbclid>`, and a reconstruction whose\ntimestamp is off by a session does not match.",
            "maxLength": 500
          },
          "fbp": {
            "type": "string",
            "maxLength": 200
          },
          "pageUrl": {
            "type": "string",
            "description": "The page the form was submitted from, for Meta's `event_source_url`.\n\nSent by the client rather than reconstructed here because the property\nwebsite is served on the ORG'S OWN verified custom domain when it has one,\nand only the browser knows which host the visitor actually used. A\nmismatched source URL lowers the event's match quality.",
            "maxLength": 2048
          },
          "unbranded": {
            "type": "boolean"
          },
          "trackingConsent": {
            "type": "object",
            "description": "`'denied'` when the visitor's stored tracking-consent choice says so.\n\n`lib/tracking-consent.ts` is the browser's one predicate and it gates the\npixel injection, the `Lead` event and the attribution fields. It could not\ngate the server-side send, because the server was never told — so flipping\nthat file's default to `denied`, which its own comments describe as the\nsingle line a compliance decision changes, would have stopped every browser\ntracker and left the Conversions API sending hashed buyer identities to Meta\nregardless. Now it is told, and the attribution row records `DISABLED`.",
            "enum": [
              "granted",
              "denied"
            ]
          }
        },
        "required": [
          "name",
          "email"
        ]
      },
      "RecordPropertyVisitDto": {
        "type": "object",
        "properties": {
          "visitorId": {
            "type": "string",
            "description": "Stable per-browser id kept in localStorage.\n\nOptional by necessity — a browser that blocks storage, or a client build\nolder than this feature, will not send one, and those visits still count.\nWhen it IS present it takes priority over the IP hash for deduplication,\nbecause an IP hash collapses a whole office behind one NAT into a single\nvisitor.",
            "maxLength": 64
          },
          "referrer": {
            "type": "string",
            "maxLength": 1000
          },
          "utmSource": {
            "type": "string",
            "maxLength": 200
          },
          "utmMedium": {
            "type": "string",
            "maxLength": 200
          },
          "utmCampaign": {
            "type": "string",
            "maxLength": 200
          },
          "utmTerm": {
            "type": "string",
            "description": "The rest of the UTM set: the keyword a search ad bid on (`utmTerm`) and\nwhich creative was clicked (`utmContent`). They answer \"which AD works\", as\nopposed to \"which CAMPAIGN\", which the three above already answer.",
            "maxLength": 200
          },
          "utmContent": {
            "type": "string",
            "maxLength": 200
          },
          "fbclid": {
            "type": "string",
            "description": "═══ CLICK IDS — captured at LANDING, because that is the only moment they\nexist ═══\n\nA UTM is for our report. A click id is the ONLY thing Meta, Google, TikTok\nor Microsoft can match a later conversion back to a specific ad click with.\nThe click happens here, on the landing; the enquiry is submitted minutes or\ndays later, often after several page views. If this route does not store the\nid, nothing downstream can invent it.\n\nAll optional, like everything else on this DTO: an organic visit has none,\nand an old cached bundle that sends none must keep counting rather than\nstart 400ing.",
            "maxLength": 400
          },
          "gclid": {
            "type": "string",
            "maxLength": 400
          },
          "ttclid": {
            "type": "string",
            "maxLength": 400
          },
          "msclkid": {
            "type": "string",
            "maxLength": 400
          },
          "fbp": {
            "type": "string",
            "description": "Meta's `_fbp` browser cookie, read verbatim from `document.cookie`. Paired\nwith `fbclid` it is what lifts a Conversions API event from \"unmatched\" to\n\"matched\"; on its own it still identifies a returning browser to Meta.",
            "maxLength": 200
          }
        }
      },
      "UpdateListingDetailsDto": {
        "type": "object",
        "properties": {
          "beds": {
            "type": "number",
            "minimum": 0
          },
          "baths": {
            "type": "number",
            "minimum": 0
          },
          "sqft": {
            "type": "number",
            "minimum": 0
          },
          "lotSizeSqft": {
            "type": "number",
            "minimum": 0
          },
          "lotAcres": {
            "type": "number",
            "minimum": 0
          },
          "parkingSpots": {
            "type": "number",
            "minimum": 0
          },
          "price": {
            "type": "number",
            "minimum": 0
          },
          "priceCurrency": {
            "type": "string"
          },
          "propertyType": {
            "type": "string",
            "enum": [
              "HOUSE",
              "CONDO",
              "TOWNHOUSE",
              "LAND",
              "COMMERCIAL",
              "INDUSTRIAL",
              "MULTI_FAMILY"
            ]
          },
          "yearBuilt": {
            "type": "number"
          },
          "mlsNumber": {
            "type": "string"
          },
          "listingType": {
            "type": "string",
            "enum": [
              "SALE",
              "RENT"
            ]
          },
          "propertyStatus": {
            "type": "string",
            "enum": [
              "DRAFT",
              "FOR_SALE",
              "FOR_RENT",
              "SOLD",
              "LEASED",
              "OFF_MARKET"
            ]
          },
          "mlsLiveDate": {
            "type": "string"
          },
          "customName": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "gatedMediaTypes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ReorderListingPhotosDto": {
        "type": "object",
        "properties": {
          "mediaIds": {
            "description": "Full grid order, 1-based positions assigned in this sequence.",
            "uniqueItems": true,
            "maxItems": 1000,
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 64
            }
          }
        },
        "required": [
          "mediaIds"
        ]
      },
      "HeroContentDto": {
        "type": "object",
        "properties": {
          "headline": {
            "type": "string",
            "maxLength": 200
          },
          "eyebrow": {
            "type": "string",
            "maxLength": 200
          },
          "heroMediaId": {
            "type": "string",
            "maxLength": 64
          },
          "ctaLabel": {
            "type": "string",
            "maxLength": 80
          }
        }
      },
      "StatCardDto": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "maxLength": 80
          },
          "value": {
            "type": "string",
            "maxLength": 80
          }
        },
        "required": [
          "label",
          "value"
        ]
      },
      "OverviewContentDto": {
        "type": "object",
        "properties": {
          "headline": {
            "type": "string",
            "maxLength": 300
          },
          "body": {
            "type": "string",
            "maxLength": 8000
          },
          "statCards": {
            "maxItems": 8,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatCardDto"
            }
          }
        }
      },
      "GalleryGroupDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "maxLength": 64
          },
          "title": {
            "type": "string",
            "maxLength": 120
          },
          "blurb": {
            "type": "string",
            "maxLength": 600
          },
          "mediaIds": {
            "maxItems": 300,
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 64
            }
          },
          "captions": {
            "type": "object",
            "description": "mediaId → caption. Values are ENFORCED to be strings here — see\nis-caption-map.validator.ts. A non-string value used to survive the pipe\nand crash the public page's SSR."
          }
        },
        "required": [
          "id",
          "title",
          "mediaIds"
        ]
      },
      "GalleryContentDto": {
        "type": "object",
        "properties": {
          "intro": {
            "type": "string",
            "maxLength": 600
          },
          "groups": {
            "maxItems": 20,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GalleryGroupDto"
            }
          },
          "hiddenMediaIds": {
            "description": "PHOTOS THE CURATOR HAS HIDDEN FROM THE PUBLIC SITE.\n\nA per-website curation decision, NOT a delete: the media stays on the\nproject, in the delivery, and in every other deliverable. It simply is not\npublished on this property website — not inside a group, not in the\ntrailing \"More photographs\" gallery, not as the hero, and not as the\nsocial-preview image.\n\nIt lives under `gallery` because photo curation already does (groups,\ncaptions), but its SCOPE IS THE WHOLE SITE: `getListingBySlugOrToken`\nremoves these ids from the public payload before anything reads it, so\nhiding still holds when the `gallery` section is excluded from\n`includedSections`.\n\nThe list may never contain the hero — `ListingsService.enforceHeroNotHidden`\nmakes that a stored invariant on every save, so no consumer has to reason\nabout a hero that isn't allowed to render.\n\nBounded like every other array here: 1000 is far above any real delivery\n(a shoot tops out in the low hundreds) and exists only to make the\npathological payload impossible.",
            "uniqueItems": true,
            "maxItems": 1000,
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 64
            }
          },
          "photoOrder": {
            "description": "THE ORDER OF THE UNGROUPED PHOTOS, AS THE WEBSITE EDITOR ARRANGED THEM.\n\nUngrouped photos publish in the trailing \"More photographs\" gallery, and\nuntil now their order came only from `Media.displayOrder` — which is set in\nthe job's Media tab, a staff-only surface. An AGENT editing their own\nproperty website therefore had no way to order their photos at all: the\nwizard's drag only worked inside a named group, and a site with no groups\nhad nothing draggable on it.\n\nDeliberately NOT written through the media reorder endpoint. That endpoint\nis gated on `canUploadMedia`, which opens with `project.orgId !== ctx.org.id`\n— and an agent editing their own listing calls from their PERSONAL org, so\nit refuses them. Reusing it would have meant widening media-write\npermissions to make a cosmetic ordering feature work, which is the wrong\ntrade. This field is authorised by `resolveMarketingEditor` instead, the\nsame gate as every other thing on this page.\n\nLEGACY — NO LONGER WRITTEN. The editor now arranges `Media.displayOrder`\nitself (see `ListingsService.reorderListingPhotos`), so there is ONE photo\norder across the website, the downloads centre and the delivery ZIP. Two\norders for one set of photos read as a bug to everyone who met them.\n\nThe field is still ACCEPTED and still applied when present, so a page\narranged before that change keeps the order it was published with instead\nof reshuffling on deploy. The first drag on a listing clears it. Ids absent\nfrom the list fall back to `displayOrder` behind the ones listed.\n\nBounded at 1000 like `hiddenMediaIds`, for the same reason.",
            "uniqueItems": true,
            "maxItems": 1000,
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 64
            }
          }
        }
      },
      "HighlightItemDto": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 120
          },
          "body": {
            "type": "string",
            "maxLength": 600
          }
        },
        "required": [
          "title",
          "body"
        ]
      },
      "HighlightsContentDto": {
        "type": "object",
        "properties": {
          "items": {
            "maxItems": 12,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/HighlightItemDto"
            }
          }
        }
      },
      "NamedNoteDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 160
          },
          "note": {
            "type": "string",
            "maxLength": 300
          }
        },
        "required": [
          "name"
        ]
      },
      "CommuteEntryDto": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "maxLength": 120
          },
          "minutes": {
            "type": "number",
            "minimum": 0,
            "maximum": 600
          }
        },
        "required": [
          "label",
          "minutes"
        ]
      },
      "NeighborhoodContentDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "blurb": {
            "type": "string",
            "maxLength": 2000
          },
          "schools": {
            "maxItems": 12,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NamedNoteDto"
            }
          },
          "commute": {
            "maxItems": 12,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CommuteEntryDto"
            }
          },
          "nearby": {
            "maxItems": 20,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NamedNoteDto"
            }
          }
        }
      },
      "LocationContentDto": {
        "type": "object",
        "properties": {
          "commuteTiles": {
            "maxItems": 12,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CommuteEntryDto"
            }
          }
        }
      },
      "TeamContentDto": {
        "type": "object",
        "properties": {
          "headline": {
            "type": "string",
            "maxLength": 200
          },
          "bio": {
            "type": "string",
            "maxLength": 4000
          },
          "statChips": {
            "maxItems": 8,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StatCardDto"
            }
          },
          "photoUrl": {
            "type": "string",
            "description": "Rendered in `<img src>` on the agency's branded domain — https only, and\nDISCARDED entirely when the editor is a customer (see\nListingsService.stripCustomerOnlyForbiddenFields).",
            "maxLength": 2048,
            "format": "uri"
          },
          "logoUrls": {
            "maxItems": 6,
            "type": "array",
            "items": {
              "type": "string",
              "format": "uri",
              "maxLength": 2048
            }
          }
        }
      },
      "FaqItemDto": {
        "type": "object",
        "properties": {
          "q": {
            "type": "string",
            "maxLength": 300
          },
          "a": {
            "type": "string",
            "maxLength": 2000
          }
        },
        "required": [
          "q",
          "a"
        ]
      },
      "FaqContentDto": {
        "type": "object",
        "properties": {
          "items": {
            "maxItems": 20,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FaqItemDto"
            }
          }
        }
      },
      "ContactContentDto": {
        "type": "object",
        "properties": {
          "headline": {
            "type": "string",
            "maxLength": 200
          },
          "blurb": {
            "type": "string",
            "maxLength": 600
          },
          "agentName": {
            "type": "string",
            "description": "PER-LISTING OVERRIDES FOR THE AGENT CARD'S FOUR TEXT FIELDS.\n\nBlank means \"use the profile\" — see resolveListingAgent, which is the only\nthing that reads these. They exist so one listing can show a campaign or\nco-listing number without rewriting the agent's profile, which would change\nevery listing they have ever had.\n\nDELIBERATELY NOT `@IsEmail` / a phone pattern. This route is a full-replace\nAUTOSAVE: the wizard PUTs the whole config as the user types, so a format\nrule here would 400 every keystroke of \"k\", \"kk\", \"kkang@\" and the editor\nwould appear to stop saving until the address happened to parse. The values\nare rendered as text and as `mailto:` / `tel:` hrefs, where a malformed one\nis inert, so the cost of accepting it is a dead link the agent can see and\nfix — not a save they cannot complete.\n\nThere is no `photoUrl` / `brokerageLogo` here on purpose: those are\n`<img src>` on the agency's own verified domain, which is exactly what\nstripCustomerOnlyForbiddenFields refuses to let a customer set on `team`.",
            "maxLength": 120
          },
          "agentPhone": {
            "type": "string",
            "maxLength": 40
          },
          "agentEmail": {
            "type": "string",
            "maxLength": 200
          },
          "brokerageName": {
            "type": "string",
            "maxLength": 200
          }
        }
      },
      "WebsiteSectionContentDto": {
        "type": "object",
        "properties": {
          "hero": {
            "$ref": "#/components/schemas/HeroContentDto"
          },
          "overview": {
            "$ref": "#/components/schemas/OverviewContentDto"
          },
          "gallery": {
            "$ref": "#/components/schemas/GalleryContentDto"
          },
          "highlights": {
            "$ref": "#/components/schemas/HighlightsContentDto"
          },
          "neighborhood": {
            "$ref": "#/components/schemas/NeighborhoodContentDto"
          },
          "location": {
            "$ref": "#/components/schemas/LocationContentDto"
          },
          "team": {
            "$ref": "#/components/schemas/TeamContentDto"
          },
          "faq": {
            "$ref": "#/components/schemas/FaqContentDto"
          },
          "contact": {
            "$ref": "#/components/schemas/ContactContentDto"
          }
        }
      },
      "UpdateWebsiteConfigDto": {
        "type": "object",
        "properties": {
          "includedSections": {
            "type": "array",
            "description": "`@ArrayUnique` because the renderer maps over this list directly: a\nduplicated key rendered the same section TWICE, with a duplicate React key\nAND a duplicate `id=` anchor — which breaks the in-page nav (both anchors\nresolve to the first) as well as React reconciliation.",
            "uniqueItems": true,
            "items": {
              "type": "string",
              "enum": [
                "hero",
                "overview",
                "gallery",
                "highlights",
                "neighborhood",
                "location",
                "team",
                "faq",
                "contact"
              ]
            }
          },
          "sectionContent": {
            "description": "`@IsDefined()` + `@IsObject()` are what make the nested tree above run at\nall. `@ValidateNested()` alone SKIPS a property that is absent — so\n`{\"includedSections\":[\"hero\"]}` validated clean and stored `undefined`,\nand `{\"includedSections\":[],\"sectionContent\":[]}` validated clean with an\nARRAY where every consumer (the public renderer, the OG-image builder, the\nbrochure path) reads `sectionContent.hero`. Two callers of the same route\ncould therefore be validated to two different contracts. `@IsObject()`\nrejects the array form specifically; class-validator's `isObject` excludes\narrays.\n\nThe only writer is `sanitizeConfigForSave`\n(apps/frontend/components/features/listing/wizard/wizard-utils.ts), which\nalways emits both keys and an object (`{}` when nothing is filled in), so\ntightening this refuses nothing that was legitimately being sent.",
            "allOf": [
              {
                "$ref": "#/components/schemas/WebsiteSectionContentDto"
              }
            ]
          },
          "templateId": {
            "type": "string",
            "nullable": true,
            "description": "WHICH SITE TEMPLATE RENDERS THIS PROPERTY.\n\n`@IsIn(SITE_TEMPLATE_IDS)` and not `@IsString()`, because this id selects a\nRENDERER on a live public page: an unrecognised value does not degrade, it\nleaves the page with nothing to draw, on a URL that is very likely already\nprinted on a brochure and attached to an MLS record. A 400 in the wizard is\ninfinitely preferable.\n\nOPTIONAL, and absent means \"leave it as it is\" — NOT \"reset to classic\".\nEvery autosave sends the whole config, and a client build that predates\ntemplates omits this key entirely; treating that omission as a reset would\nsilently pull every listing back to classic the moment one stale tab saved.\nClearing the choice is an explicit `null`.\n\nNULL renders classic — the look every already-published property website has\ntoday. Nothing is backfilled to a new template; see `site-templates.ts`.",
            "enum": [
              "classic",
              "editorial",
              "warm"
            ]
          },
          "metaPixelId": {
            "type": "string",
            "nullable": true,
            "description": "═══ PER-LISTING AD PIXELS — the override of the company default ═══\n\nThe agent running ads on their own listing uses their own pixel; the company\nsets a house default on `Organization.default*` and this overrides it, per\nfield. Same optional-means-leave-alone / null-means-clear rule as\n`templateId`, for the same reason.\n\nIDS ONLY, AND THAT IS A BOUNDARY. These are served to the PUBLIC property\nwebsite so the browser can load the tag — a pixel id is public by\nconstruction, it ships inside the page's own script. The Conversions API\nACCESS TOKEN is a credential and is not settable here at all: it lives\nencrypted on `OrgAdTrackingCredential`, reachable only through the\nowner/admin-only `/ad-tracking` routes. A secret accepted on this route\nwould be published on the open internet by design, and it would be published\nby the CUSTOMER, who can also edit this config.\n\n── AND THE CHARACTER SET IS A BOUNDARY TOO ──────────────────────────────\n\n`@Matches(PIXEL_ID_PATTERN)` and not a bare `@IsString()`. These ids are\ninterpolated into the BODY of an inline `<script>` on a public property\nwebsite (`fbq('init', …)` / `gtag('config', …)`), and THIS route is one a\nCUSTOMER may call — so the value is attacker-influenced text on its way into\nscript source. `JSON.stringify` escapes it as a JS string, but a JS string\nis not an HTML boundary: a `</script>` inside one ends the element for the\nparser and everything after it is markup. The same rule is enforced again in\nthe service (`cleanPixelId`) because that is the choke point both writers —\nthis route and the org defaults — share; this decorator is what turns it\ninto a legible 400 in the wizard instead of a generic one.\n\n`@MaxLength` stays: the pattern bounds length too, but the two are read by\ndifferent people for different reasons and the column is 120 either way.\n\n`''` still clears — `@Matches` is skipped by `@IsOptional` for null, and the\nempty string is caught by the service's blank check before the pattern.",
            "maxLength": 120
          },
          "ga4MeasurementId": {
            "type": "string",
            "nullable": true,
            "maxLength": 120
          },
          "googleAdsConversionId": {
            "type": "string",
            "nullable": true,
            "maxLength": 120
          },
          "googleAdsConversionLabel": {
            "type": "string",
            "nullable": true,
            "maxLength": 120
          },
          "accentPrimary": {
            "type": "string",
            "nullable": true,
            "description": "═══ THE PAGE'S ACCENT PAIR — the agent's brand, not the agency's ═══\n\nA property website is produced by the media company and handed to a SELLER\nBY AN AGENT who belongs to a brokerage with its own colours. Until these\nfields existed the page painted `Organization.brandColor` — the\nPHOTOGRAPHY COMPANY's colour — and nothing could override it. These are\nthat override, per listing.\n\nSame optional-means-leave-alone / null-means-clear rule as `templateId` and\nthe pixels above, for the same reason: every autosave sends the whole\nconfig, and a client build that predates these fields omits them entirely.\nTreating that omission as \"clear\" would let one stale tab silently repaint\na live client-facing page.\n\n── `@Matches`, NOT `@IsString()`, AND THAT IS A SECURITY BOUNDARY ────────\n\nThese values are interpolated into CSS custom properties on a PUBLIC,\nunauthenticated property website, and THIS route is one a CUSTOMER may\ncall — so the value is attacker-influenced text on its way into a\nstylesheet. Today it lands in a React `style` object (set through CSSOM,\ninert), but \"the current consumer happens to be safe\" is not a boundary:\nthe next consumer is a `<style>` block or an OG-image renderer, and there\n`red;} body{display:none` is markup control rather than a colour. Hex or\nnothing, refused at the write where there is a human to tell.\n\nThe same rule is enforced again in the service (`cleanAccentHex`) because\nthat is the choke point that also canonicalises `#ABC` → `#aabbcc`; this\ndecorator is what turns a bad value into a legible 400 in the wizard\ninstead of a generic one.\n\nCLEARING IS AN EXPLICIT `null`, and only `null`. `@IsOptional` skips\nvalidation for `null`/`undefined` but NOT for `''`, which therefore fails\n`@Matches` and 400s — so the wizard sends `null` to go back to inheriting.\n(The service's `cleanAccentHex` treats `''` as a clear as well, which is\nwhat keeps a non-pipe internal caller from storing an empty string.)",
            "maxLength": 7
          },
          "accentSecondary": {
            "type": "string",
            "nullable": true,
            "maxLength": 7
          }
        },
        "required": [
          "includedSections",
          "sectionContent"
        ]
      },
      "LockWebsiteConfigDto": {
        "type": "object",
        "properties": {
          "locked": {
            "type": "boolean"
          }
        },
        "required": [
          "locked"
        ]
      },
      "UpdateOrgAdDefaultsDto": {
        "type": "object",
        "properties": {
          "defaultMetaPixelId": {
            "type": "string",
            "nullable": true,
            "maxLength": 120
          },
          "defaultGa4MeasurementId": {
            "type": "string",
            "nullable": true,
            "maxLength": 120
          },
          "defaultGoogleAdsConversionId": {
            "type": "string",
            "nullable": true,
            "maxLength": 120
          },
          "defaultGoogleAdsConversionLabel": {
            "type": "string",
            "nullable": true,
            "maxLength": 120
          }
        }
      },
      "SaveAdCredentialDto": {
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "enum": [
              "META",
              "GOOGLE"
            ]
          },
          "externalId": {
            "type": "string",
            "description": "The pixel / measurement id this token authenticates FOR. Required, because\nthe credential is scoped to one pixel: that is what lets an agent's own\npixel on one listing have its own token alongside the company's.",
            "maxLength": 120
          },
          "accessToken": {
            "type": "string",
            "description": "Meta system-user access token. Long — Meta's are ~200 chars and can be\nlonger — so the bound is generous rather than tight; the real check is that\nthe platform accepts it before anything is stored.",
            "maxLength": 1000
          },
          "label": {
            "type": "string",
            "maxLength": 120
          }
        },
        "required": [
          "provider",
          "externalId",
          "accessToken"
        ]
      },
      "GenerateCopyDto": {
        "type": "object",
        "properties": {
          "section": {
            "type": "object",
            "enum": [
              "hero",
              "overview",
              "gallery",
              "highlights",
              "neighborhood",
              "location",
              "team",
              "faq",
              "contact",
              "all"
            ]
          },
          "hints": {
            "type": "string",
            "description": "Free-text bullets from the user — selling points, neighborhood\n facts, anything the fact sheet can't know.",
            "maxLength": 2000
          },
          "tone": {
            "type": "object",
            "enum": [
              "luxury",
              "warm",
              "minimal",
              "family"
            ]
          }
        },
        "required": [
          "section"
        ]
      },
      "CreateTourNodeDto": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string"
          },
          "roomId": {
            "type": "string"
          },
          "floorRoomId": {
            "type": "string"
          },
          "floorId": {
            "type": "string"
          },
          "panoramaUrl": {
            "type": "string"
          },
          "panoramaKey": {
            "type": "string"
          },
          "thumbnailUrl": {
            "type": "string"
          },
          "heading": {
            "type": "number"
          },
          "pitch": {
            "type": "number"
          },
          "positionX": {
            "type": "number"
          },
          "positionY": {
            "type": "number"
          },
          "displayOrder": {
            "type": "number"
          }
        },
        "required": [
          "label"
        ]
      },
      "UpdateTourNodeDto": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string"
          },
          "panoramaUrl": {
            "type": "string"
          },
          "panoramaKey": {
            "type": "string"
          },
          "thumbnailUrl": {
            "type": "string"
          },
          "heading": {
            "type": "number"
          },
          "frontAngle": {
            "type": "number"
          },
          "pitch": {
            "type": "number"
          },
          "positionX": {
            "type": "number"
          },
          "positionY": {
            "type": "number"
          },
          "displayOrder": {
            "type": "number"
          },
          "hidden": {
            "type": "boolean"
          }
        }
      },
      "UpdateImageEditsDto": {
        "type": "object",
        "properties": {
          "imageEdits": {
            "type": "object",
            "nullable": true
          }
        }
      },
      "CreateHotspotDto": {
        "type": "object",
        "properties": {
          "targetNodeId": {
            "type": "string"
          },
          "yaw": {
            "type": "number"
          },
          "pitch": {
            "type": "number"
          },
          "placed": {
            "type": "boolean"
          },
          "label": {
            "type": "string"
          },
          "hotspotType": {
            "type": "object"
          }
        },
        "required": [
          "targetNodeId"
        ]
      },
      "UpdateHotspotDto": {
        "type": "object",
        "properties": {
          "yaw": {
            "type": "number"
          },
          "pitch": {
            "type": "number"
          },
          "label": {
            "type": "string"
          }
        }
      },
      "UpdateTourSettingsDto": {
        "type": "object",
        "properties": {
          "showMinimap": {
            "type": "boolean",
            "description": "Render the floor-plan minimap in the PUBLISHED viewer. Defaults to true at\nthe column, so omitting this never turns an existing tour's minimap off.\nThe tour EDITOR always keeps its minimap regardless — this is not an\nauthoring setting."
          },
          "startNodeId": {
            "type": "string",
            "nullable": true,
            "description": "WHERE THE TOUR OPENS — the id of the TourNode a visitor lands on.\n\nDistinct from a FLOOR's start node (`.../floors/:floorId/start-node`), which\ngoverns floor switches. Without this the viewer opened on `floors[0]`, and\nfloors sort by level ascending, so every multi-floor tour opened in the\nbasement.\n\n`null` CLEARS it (back to the lowest floor's start pano) — which is why the\nvalidator has to allow null explicitly; omitting the key leaves it alone.\nThe service verifies a non-null id is a pano on THIS property before\nstoring it."
          }
        }
      },
      "PublishJobSpotDto": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string"
          },
          "panoramaUrl": {
            "type": "string"
          },
          "panoramaKey": {
            "type": "string"
          },
          "positionX": {
            "type": "number"
          },
          "positionY": {
            "type": "number"
          },
          "heading": {
            "type": "number"
          },
          "floorId": {
            "type": "string"
          },
          "displayOrder": {
            "type": "number"
          }
        },
        "required": [
          "label",
          "panoramaUrl",
          "positionX",
          "positionY"
        ]
      },
      "EnqueuePublishJobDto": {
        "type": "object",
        "properties": {
          "spots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublishJobSpotDto"
            }
          },
          "projectId": {
            "type": "string"
          },
          "force": {
            "type": "boolean"
          }
        },
        "required": [
          "spots"
        ]
      },
      "RunPodOutputMediaItemDto": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string"
          },
          "filename": {
            "type": "string"
          },
          "thumbnail_url": {
            "type": "string"
          }
        },
        "required": [
          "url",
          "filename"
        ]
      },
      "RunPodOutputDto": {
        "type": "object",
        "properties": {
          "videos": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RunPodOutputMediaItemDto"
            }
          },
          "images": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RunPodOutputMediaItemDto"
            }
          }
        }
      },
      "RunPodWebhookDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "output": {
            "$ref": "#/components/schemas/RunPodOutputDto"
          },
          "error": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "status"
        ]
      },
      "AiImageClassificationDto": {
        "type": "object",
        "properties": {
          "mediaId": {
            "type": "string"
          },
          "environment": {
            "type": "string",
            "enum": [
              "EXTERIOR",
              "INTERIOR"
            ]
          },
          "season": {
            "type": "string",
            "enum": [
              "SPRING",
              "SUMMER",
              "FALL",
              "WINTER"
            ]
          },
          "furnished": {
            "type": "boolean"
          },
          "roomType": {
            "type": "string"
          },
          "style": {
            "type": "string"
          }
        },
        "required": [
          "mediaId",
          "environment"
        ]
      },
      "SubmitAiEnhanceDto": {
        "type": "object",
        "properties": {
          "images": {
            "minItems": 1,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AiImageClassificationDto"
            }
          },
          "workflowId": {
            "type": "string",
            "description": "Optional workflow override. Defaults to cinematic-long-video when omitted.",
            "enum": [
              "cinematic-long-video",
              "viral-short-video",
              "photo-enhance",
              "bracket-merge",
              "day-to-dusk",
              "photo-touch-up",
              "virtual-stage",
              "object-removal",
              "hdr-image"
            ]
          }
        },
        "required": [
          "images"
        ]
      },
      "SubmitAiRevisionDto": {
        "type": "object",
        "properties": {
          "mediaId": {
            "type": "string"
          },
          "feedback": {
            "type": "string",
            "maxLength": 200
          },
          "quickTags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "mediaId",
          "feedback"
        ]
      },
      "SubmitAiApproveDto": {
        "type": "object",
        "properties": {
          "mediaId": {
            "type": "string"
          }
        },
        "required": [
          "mediaId"
        ]
      },
      "EnhancementOptionsDto": {
        "type": "object",
        "properties": {
          "style": {
            "type": "string",
            "description": "DERIVED, for the same reason `WORKFLOW_IDS` above is. A hand-copied list\nof looks would go stale the day a fifth is added and `whitelist: true`\nwould then reject it with a 400 that says nothing about why.",
            "enum": [
              "natural",
              "warm",
              "neutral",
              "vibrant"
            ]
          },
          "verticalCorrection": {
            "type": "boolean",
            "description": "Straighten converging verticals."
          },
          "lensCorrection": {
            "type": "boolean",
            "description": "Correct wide-lens distortion."
          },
          "windowPull": {
            "type": "boolean",
            "description": "Recover blown-out window views."
          },
          "autoPrivacy": {
            "type": "boolean",
            "description": "Blur faces and licence plates."
          },
          "skyReplacement": {
            "type": "boolean",
            "description": "Replace a flat sky."
          },
          "skyStyle": {
            "type": "string",
            "description": "Evening sky for a day-to-dusk job.",
            "enum": [
              "dusk"
            ]
          },
          "blankTvScreens": {
            "type": "boolean",
            "description": "Black out television screens showing a frozen frame or reflection."
          },
          "lightFireplaces": {
            "type": "boolean",
            "description": "Put a fire in the fireplaces."
          },
          "greenLawn": {
            "type": "boolean",
            "description": "Bring a tired or patchy lawn back to green."
          },
          "removePhotographer": {
            "type": "boolean",
            "description": "Take the photographer out of a mirror, wardrobe door or window reflection."
          },
          "eveningLight": {
            "type": "boolean",
            "description": "Relight the whole scene for evening. Not available — a request setting this is refused, and nothing is charged."
          },
          "stagedRoomType": {
            "type": "string",
            "description": "What a bare room should be furnished as. Virtual staging only — no engine is available for it yet, and a job requesting it is refused.",
            "enum": [
              "living-room",
              "bedroom",
              "dining-room",
              "kitchen",
              "home-office",
              "outdoor"
            ]
          },
          "furnitureStyle": {
            "type": "string",
            "enum": [
              "modern",
              "scandinavian",
              "traditional",
              "coastal",
              "farmhouse",
              "midcentury",
              "luxury",
              "contemporary"
            ],
            "description": "The furniture look for a staged room. Virtual staging only — not available yet."
          },
          "removalScope": {
            "type": "string",
            "enum": [
              "furniture",
              "clutter"
            ],
            "description": "How much an emptying pass takes out. Furniture removal only — not available yet."
          },
          "maskKey": {
            "type": "string",
            "description": "Object removal only. The S3 key of a painted mask uploaded to this project. The service verifies the key stays within the caller's own project before it is used."
          }
        }
      },
      "CreateAiStudioProjectDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "images": {
            "minItems": 1,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AiImageClassificationDto"
            }
          },
          "mediaKeys": {
            "minItems": 1,
            "description": "Media storage keys (S3 paths)",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "workflowId": {
            "type": "string",
            "enum": [
              "cinematic-long-video",
              "viral-short-video",
              "photo-enhance",
              "bracket-merge",
              "day-to-dusk",
              "photo-touch-up",
              "virtual-stage",
              "object-removal",
              "hdr-image"
            ],
            "description": "Optional workflow override. Defaults to cinematic-long-video."
          },
          "projectId": {
            "type": "string",
            "description": "Vremly Project this AI job is anchored to. When supplied, outputs save back into the project's Media collection and the AI run appears in the project's history."
          },
          "enhancement": {
            "description": "Per-job enhancement choices. Only meaningful for enhancement workflows; ignored by the prompt-driven ones.",
            "allOf": [
              {
                "$ref": "#/components/schemas/EnhancementOptionsDto"
              }
            ]
          }
        },
        "required": [
          "images",
          "mediaKeys"
        ]
      },
      "AiBracketGroupDto": {
        "type": "object",
        "properties": {
          "mediaIds": {
            "description": "The exposures, darkest-to-brightest as captured. Two is the smallest set\nthat is a bracket at all; nine is well past any real camera's sequence and\nis here to stop a mis-built client sending a whole shoot as one group.",
            "minItems": 2,
            "maxItems": 9,
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "environment": {
            "type": "string",
            "enum": [
              "EXTERIOR",
              "INTERIOR"
            ]
          },
          "season": {
            "type": "string",
            "enum": [
              "SPRING",
              "SUMMER",
              "FALL",
              "WINTER"
            ]
          },
          "furnished": {
            "type": "boolean"
          },
          "roomType": {
            "type": "string"
          },
          "style": {
            "type": "string"
          }
        },
        "required": [
          "mediaIds",
          "environment"
        ]
      },
      "SubmitAiBracketsDto": {
        "type": "object",
        "properties": {
          "groups": {
            "minItems": 1,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AiBracketGroupDto"
            }
          },
          "enhancement": {
            "description": "Per-job enhancement choices, applied to the merged photo. Visible AI disclosure is deliberately absent — it is the org’s setting.",
            "allOf": [
              {
                "$ref": "#/components/schemas/EnhancementOptionsDto"
              }
            ]
          }
        },
        "required": [
          "groups"
        ]
      },
      "EditImageDto": {
        "type": "object",
        "properties": {
          "treatment": {
            "type": "string",
            "enum": [
              "virtual-stage",
              "object-removal",
              "day-to-dusk",
              "photo-touch-up"
            ],
            "description": "Which treatment to apply to this one photo. A treatment with no engine available is refused before any work is created."
          },
          "options": {
            "description": "The choices the operator made in the panel, in OUR vocabulary.\n\nEvery field is optional and every field is validated. A client that sends\nan option this treatment does not use is not an error — the provider reads\nonly what its capability honours, and the capabilities endpoint is what\nstopped the screen offering it in the first place.",
            "allOf": [
              {
                "$ref": "#/components/schemas/EnhancementOptionsDto"
              }
            ]
          }
        },
        "required": [
          "treatment"
        ]
      },
      "CreateBrochureDto": {
        "type": "object",
        "properties": {
          "projectId": {
            "type": "string"
          },
          "templateId": {
            "type": "string"
          },
          "variant": {
            "type": "string",
            "enum": [
              "1-page"
            ]
          },
          "format": {
            "type": "string",
            "enum": [
              "digital",
              "print"
            ]
          },
          "overrides": {
            "type": "object",
            "properties": {
              "headline": {
                "type": "string"
              },
              "description": {
                "type": "string"
              },
              "price": {
                "type": "string"
              },
              "features": {
                "type": "string"
              }
            },
            "required": []
          },
          "selectedMediaIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "projectId",
          "templateId"
        ]
      },
      "CreateCourseDto": {
        "type": "object",
        "properties": {}
      },
      "UpdateCourseDto": {
        "type": "object",
        "properties": {}
      },
      "CreateStepDto": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "contentType": {
            "type": "string",
            "enum": [
              "text",
              "image",
              "video",
              "document"
            ]
          },
          "content": {
            "type": "object",
            "description": "{ text?: string, url?: string, caption?: string }"
          },
          "sectionTitle": {
            "type": "string",
            "description": "Sidebar section grouping for classroom courses"
          }
        },
        "required": [
          "title",
          "contentType",
          "content"
        ]
      },
      "UpdateStepDto": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "contentType": {
            "type": "string",
            "enum": [
              "text",
              "image",
              "video",
              "document"
            ]
          },
          "content": {
            "type": "object"
          },
          "sectionTitle": {
            "type": "string",
            "description": "Sidebar section grouping for classroom courses"
          }
        }
      },
      "ReorderStepsDto": {
        "type": "object",
        "properties": {
          "stepIds": {
            "description": "Ordered array of step IDs",
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "stepIds"
        ]
      },
      "CreateAnnouncementDto": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "maxLength": 500
          },
          "linkLabel": {
            "type": "string",
            "nullable": true,
            "maxLength": 80
          },
          "linkUrl": {
            "type": "string",
            "nullable": true,
            "maxLength": 2048
          },
          "bodyHtml": {
            "type": "string",
            "nullable": true,
            "description": "Long-form rich-text body for the \"Read more\" dialog. Raw editor HTML —\nthe service sanitizes it to an allowlist before it is stored, so this\nlength cap bounds the pre-sanitize payload, not the trusted output.",
            "maxLength": 20000
          },
          "readMoreLabel": {
            "type": "string",
            "nullable": true,
            "maxLength": 40
          },
          "style": {
            "type": "object"
          },
          "startAt": {
            "type": "string",
            "nullable": true
          },
          "endAt": {
            "type": "string",
            "nullable": true
          },
          "isActive": {
            "type": "boolean"
          },
          "dismissible": {
            "type": "boolean"
          }
        },
        "required": [
          "message"
        ]
      },
      "UpdateAnnouncementDto": {
        "type": "object",
        "properties": {}
      },
      "CreateSpecialtyDto": {
        "type": "object",
        "properties": {}
      },
      "UpdateSpecialtyDto": {
        "type": "object",
        "properties": {}
      },
      "CreateCertificationLevelDto": {
        "type": "object",
        "properties": {}
      },
      "UpdateCertificationLevelDto": {
        "type": "object",
        "properties": {}
      },
      "CreateModuleDto": {
        "type": "object",
        "properties": {}
      },
      "UpdateModuleDto": {
        "type": "object",
        "properties": {}
      },
      "CreateLessonDto": {
        "type": "object",
        "properties": {}
      },
      "UpdateLessonDto": {
        "type": "object",
        "properties": {}
      },
      "UpsertQuizDto": {
        "type": "object",
        "properties": {}
      },
      "SubmitQuizAttemptDto": {
        "type": "object",
        "properties": {}
      },
      "CreatePracticalEvalDto": {
        "type": "object",
        "properties": {}
      },
      "DecidePracticalEvalDto": {
        "type": "object",
        "properties": {}
      },
      "CreateResourceDto": {
        "type": "object",
        "properties": {}
      },
      "UpdateResourceDto": {
        "type": "object",
        "properties": {}
      },
      "ToggleChecklistItemDto": {
        "type": "object",
        "properties": {}
      },
      "SubmitReadinessCheckDto": {
        "type": "object",
        "properties": {}
      },
      "SubmitDebriefDto": {
        "type": "object",
        "properties": {}
      },
      "CreateBookingQuestionSetDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 160,
            "description": "Display name for the question set (e.g., \"Social Media Questions\"). Unique per org."
          },
          "description": {
            "type": "string",
            "description": "Optional internal description of when to use this set."
          },
          "questions": {
            "description": "BookingQuestion[] — questions in this set.",
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "displayOrder": {
            "type": "number",
            "minimum": 0,
            "description": "Display order in the admin list"
          },
          "applyToAllPackages": {
            "type": "boolean",
            "description": "When true, this set is applied to every booking on this org regardless of selected package or add-ons."
          },
          "applyToPackageIds": {
            "description": "IDs of ServicePackage rows this set applies to.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "applyToAddOnIds": {
            "description": "IDs of PackageAddOn rows this set applies to.",
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "name"
        ]
      },
      "UpdateBookingQuestionSetDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 160
          },
          "description": {
            "type": "string"
          },
          "questions": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "displayOrder": {
            "type": "number",
            "minimum": 0
          },
          "applyToAllPackages": {
            "type": "boolean"
          },
          "applyToPackageIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "applyToAddOnIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "CreateTechnicianRequestDto": {
        "type": "object",
        "properties": {
          "projectId": {
            "type": "string",
            "description": "Project the request is for."
          },
          "requesterContactEmail": {
            "type": "string",
            "format": "email",
            "description": "Email of the customer placing the request — used to verify ownership against the project customer record."
          },
          "requestedTechnicianId": {
            "type": "string",
            "description": "Specific technician the customer wants. Null = \"anyone different\"."
          },
          "customerNote": {
            "type": "string",
            "maxLength": 2000,
            "description": "Optional note from the customer."
          }
        },
        "required": [
          "projectId",
          "requesterContactEmail"
        ]
      },
      "ResolveTechnicianRequestDto": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "Outcome — accept reassigns the project to the requested tech (or another picked tech); decline keeps current assignment.",
            "enum": [
              "ACCEPTED",
              "DECLINED"
            ]
          },
          "assignTechnicianId": {
            "type": "string",
            "description": "When accepting, optionally override the technician to assign (e.g., admin picks a different tech than the customer requested)."
          },
          "resolverNote": {
            "type": "string",
            "maxLength": 2000,
            "description": "Optional note from the resolving admin to be relayed back to the customer."
          }
        },
        "required": [
          "status"
        ]
      },
      "CreatePipelineStageDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 60
          },
          "color": {
            "type": "string",
            "maxLength": 32
          },
          "mappedStatus": {
            "type": "object",
            "nullable": true
          },
          "triggers": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "name"
        ]
      },
      "ReorderPipelineStagesDto": {
        "type": "object",
        "properties": {
          "stageIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "stageIds"
        ]
      },
      "UpdatePipelineStageDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 60
          },
          "color": {
            "type": "string",
            "maxLength": 32
          },
          "mappedStatus": {
            "type": "object",
            "nullable": true
          },
          "triggers": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sortOrder": {
            "type": "number"
          }
        }
      },
      "CreateRoomTypeDto": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "maxLength": 60
          },
          "fillColor": {
            "type": "string"
          }
        },
        "required": [
          "label",
          "fillColor"
        ]
      },
      "ReorderRoomTypesDto": {
        "type": "object",
        "properties": {
          "orderedIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "orderedIds"
        ]
      },
      "UpdateRoomTypeDto": {
        "type": "object",
        "properties": {
          "label": {
            "type": "string",
            "maxLength": 60
          },
          "fillColor": {
            "type": "string"
          }
        }
      },
      "CreateShadowJobDto": {
        "type": "object",
        "properties": {
          "shadowCompanyId": {
            "type": "string"
          },
          "propertyAddress": {
            "type": "string",
            "maxLength": 500
          },
          "propertyCity": {
            "type": "string"
          },
          "propertyRegion": {
            "type": "string"
          },
          "propertyPostalCode": {
            "type": "string"
          },
          "propertyLat": {
            "type": "number"
          },
          "propertyLng": {
            "type": "number"
          },
          "servicesRequested": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "preferredDate": {
            "type": "string"
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          },
          "squareFootage": {
            "type": "number",
            "minimum": 0
          },
          "budgetMinCents": {
            "type": "number",
            "minimum": 0
          },
          "budgetMaxCents": {
            "type": "number",
            "minimum": 0
          }
        },
        "required": [
          "shadowCompanyId",
          "propertyAddress",
          "servicesRequested"
        ]
      },
      "UpdateShadowJobStatusDto": {
        "type": "object",
        "properties": {
          "status": {
            "type": "object"
          },
          "note": {
            "type": "string",
            "maxLength": 500
          }
        },
        "required": [
          "status"
        ]
      },
      "CreateFeedbackDto": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "minLength": 1,
            "maxLength": 5000
          },
          "category": {
            "type": "object"
          },
          "url": {
            "type": "string",
            "maxLength": 500
          },
          "userAgent": {
            "type": "string",
            "maxLength": 500
          },
          "email": {
            "type": "string",
            "maxLength": 255
          },
          "name": {
            "type": "string",
            "maxLength": 255
          }
        },
        "required": [
          "message"
        ]
      },
      "CreateWhitelabelAppRequestDto": {
        "type": "object",
        "properties": {
          "appName": {
            "type": "string",
            "minLength": 2,
            "maxLength": 30,
            "description": "Display name for the iOS app — visible under the icon on the home screen + in Settings. Typically the company brand name. 2-30 chars."
          },
          "brandColor": {
            "type": "string",
            "pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$",
            "description": "Hex primary brand color baked into the iOS build. Drives the app tint (buttons, accents, splash backdrop). Format \"#RRGGBB\".",
            "example": "#1f1f1f"
          },
          "splashColor": {
            "type": "string",
            "pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$",
            "description": "Hex splash background color. Usually #ffffff or an off-white tinted with the brand color. Optional — defaults to #ffffff.",
            "example": "#ffffff"
          },
          "brandLogoUrl": {
            "type": "string",
            "format": "uri",
            "description": "URL to the hosted brand logo (square PNG, transparent background recommended). Rendered on Splash, Auth header, and the in-app brand mark. The webapp form uploads the file to the same storage used for org logos and forwards the resulting URL here."
          },
          "supportEmail": {
            "type": "string",
            "format": "email",
            "description": "Support email shown in the app Settings."
          },
          "privacyPolicyUrl": {
            "type": "string",
            "format": "uri",
            "description": "Privacy policy URL — required by Apple for App Store review."
          },
          "termsUrl": {
            "type": "string",
            "format": "uri",
            "description": "Terms of service URL."
          },
          "requesterNotes": {
            "type": "string",
            "maxLength": 2000,
            "description": "Free-form notes the company wants the Vremly team to know during review — special requirements, launch deadlines, etc."
          }
        },
        "required": [
          "appName",
          "brandColor",
          "supportEmail"
        ]
      },
      "SaveAscCredentialDto": {
        "type": "object",
        "properties": {
          "keyId": {
            "type": "string",
            "minLength": 4,
            "maxLength": 64,
            "description": "App Store Connect API key ID (the JWT \"kid\")."
          },
          "issuerId": {
            "type": "string",
            "minLength": 8,
            "maxLength": 64,
            "description": "Issuer ID for the App Store Connect API key (a UUID)."
          },
          "privateKey": {
            "type": "string",
            "minLength": 100,
            "maxLength": 5000,
            "description": "The downloaded .p8 private key contents (PKCS#8 EC key). Accepts the raw PEM or a base64 blob. Encrypted at rest; never returned."
          }
        },
        "required": [
          "keyId",
          "issuerId",
          "privateKey"
        ]
      },
      "UpdateWhitelabelAppRequestDto": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "description": "Target status. Must follow the allowed transitions.",
            "enum": [
              "PENDING_REVIEW",
              "APPROVED",
              "BUILDING",
              "READY",
              "LIVE",
              "REJECTED"
            ]
          },
          "bundleId": {
            "type": "string",
            "pattern": "^[a-z0-9]+(\\.[a-z0-9]+)+$",
            "description": "Apple bundle ID for the new app. Must be unique across all whitelabel builds. Conventionally com.<companydomain>.portal. Required before the CI workflow can run."
          },
          "ascAppId": {
            "type": "string",
            "description": "App Store Connect app record ID (numeric)."
          },
          "reviewerNotes": {
            "type": "string",
            "maxLength": 4000,
            "description": "Free-form notes from the reviewer (e.g. rejection reason, what is blocking the build, TestFlight invite instructions)."
          },
          "testflightUrl": {
            "type": "string",
            "format": "uri",
            "description": "TestFlight invite URL — stamped by the CI workflow once the build uploads successfully. Surfaced back to the requesting company."
          },
          "appStoreUrl": {
            "type": "string",
            "format": "uri",
            "description": "App Store URL — set once Apple approves the app."
          }
        }
      },
      "CreateSavedViewDto": {
        "type": "object",
        "properties": {
          "pageKey": {
            "type": "string",
            "minLength": 1,
            "maxLength": 48
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "filters": {
            "type": "object"
          },
          "displayOrder": {
            "type": "number"
          }
        },
        "required": [
          "pageKey",
          "name",
          "filters"
        ]
      },
      "UpdateSavedViewDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80
          },
          "filters": {
            "type": "object"
          },
          "displayOrder": {
            "type": "number"
          }
        }
      },
      "UpdatePayrollConfigDto": {
        "type": "object",
        "properties": {
          "payrollEnabled": {
            "type": "boolean"
          },
          "payrollMileageRateCents": {
            "type": "number",
            "nullable": true,
            "description": "Travel rates accept an explicit `null` to mean \"not configured\".\n\n`@IsOptional()` alone already skipped null, but nothing DECLARED that null\nwas a legitimate value, and the settings form omitted the key entirely when\na rate was cleared — so `updateConfig`'s skip-undefined loop left the old\nnumber in place and the only reachable \"off\" value was `0`. A stored 0 then\nreads as unconfigured downstream, which is why a deliberate zero and a\nnever-set rate became indistinguishable. Null is now the way to say \"unset\".",
            "minimum": 0
          },
          "payrollDriveTimeRateCents": {
            "type": "number",
            "nullable": true,
            "minimum": 0
          },
          "payrollDriveTimeRateMode": {
            "type": "object"
          },
          "payrollDistanceUnit": {
            "type": "object"
          },
          "payrollReimburseCommute": {
            "type": "boolean"
          },
          "payrollAttendanceMode": {
            "type": "object"
          },
          "payrollTravelOrigin": {
            "type": "object",
            "description": "── `payrollHoursBasis` AND `payrollContractedOverrunToleranceMinutes` ARE\n   NO LONGER SETTABLE ─────────────────────────────────────────────────────\n\nThe basis chose which clock a shoot was paid on — the contract, the device\nstopwatch (ACTUAL), or whichever was longer (GREATER_OF). Pay is the SHOOT's\nown duration now, always, so a setting that claimed to change it would be a\ncontrol that changes nothing while reading as though it does. The tolerance\nwas GREATER_OF-only and has nothing left to gate.\n\n`class-validator` runs with `whitelist: true`, so a client still sending\neither key has it silently stripped rather than 400ing — which is the right\noutcome here: an old browser tab must not be able to write a policy nothing\nhonours. The COLUMNS are untouched (all 902 production orgs read CONTRACTED\nalready) so historical runs keep their descriptions."
          },
          "payrollSchedule": {
            "type": "object"
          },
          "payrollPeriodAnchor": {
            "type": "number",
            "minimum": 0,
            "maximum": 31
          }
        }
      },
      "CreatePayRateVersionDto": {
        "type": "object",
        "properties": {
          "effectiveFrom": {
            "type": "string",
            "description": "The instant these rates became effective, ISO-8601.\n\nA date-only value ('2026-06-01') is read as UTC midnight — pass a full\ntimestamp if the boundary matters in your local timezone. Must not be in\nthe future: this endpoint corrects history, it does not schedule."
          },
          "payrollMileageRateCents": {
            "type": "number",
            "nullable": true,
            "description": "Omit to inherit whatever was already in force at `effectiveFrom`; pass\n `null` to say \"not configured\" (which is NOT the same as zero).",
            "minimum": 0
          },
          "payrollDriveTimeRateCents": {
            "type": "number",
            "nullable": true,
            "description": "Same inherit/null semantics as `payrollMileageRateCents`. A deliberate 0\n means \"we do not reimburse drive time\" and is honoured silently; `null`\n means nobody has decided, and the line gets flagged instead.",
            "minimum": 0
          },
          "payrollDriveTimeRateMode": {
            "type": "object"
          },
          "reason": {
            "type": "string",
            "description": "Why the past is being corrected. REQUIRED on a backdated write (enforced in\nthe service, not here, because it depends on `effectiveFrom` vs now) — this\nis money, and an audit row that says only \"someone changed the June rate\"\nis not an audit trail.",
            "maxLength": 500
          }
        },
        "required": [
          "effectiveFrom"
        ]
      },
      "CreatePayrollRunDto": {
        "type": "object",
        "properties": {
          "periodStart": {
            "type": "string",
            "description": "First day to pay. Normalised to local midnight in the ORG's timezone."
          },
          "periodEnd": {
            "type": "string",
            "description": "LAST day to pay — inclusive, as an operator means it. Normalised to the\nEXCLUSIVE start of the following local day, so 07-01 → 07-31 pays all of the\n31st and 08-01 → 08-31 starts exactly where it stopped: no boundary instant\npayable twice, and no 24-hour dead zone between periods."
          },
          "allowOverlap": {
            "type": "boolean",
            "description": "Deliberate operator override to compute a window that overlaps an existing\nrun. Does NOT re-enable double payment — anything a finalized run has already\nclaimed still shows at zero — it only permits the overlap on purpose. Without\nit an overlapping window is refused with a 409 naming the clashing runs."
          },
          "payDate": {
            "type": "string",
            "description": "Intended payment date. Required before generating a provider handoff."
          }
        },
        "required": [
          "periodStart",
          "periodEnd"
        ]
      },
      "PreviewPayrollDto": {
        "type": "object",
        "properties": {
          "periodStart": {
            "type": "string"
          },
          "periodEnd": {
            "type": "string",
            "description": "Inclusive last day, same semantics as CreatePayrollRunDto."
          }
        },
        "required": [
          "periodStart",
          "periodEnd"
        ]
      },
      "ApplyTodaysRulesDto": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "description": "WHY. Recorded on the `payroll.incentive_rule.backdated` audit entry for every\nrule this writes, exactly as a hand-written backdated rule records one —\nthis awards a bonus on work that is already finished.\n\nOptional at the API because the server always composes one naming the run\nand its period when it is omitted. An operator's own sentence is better\nevidence than a generated one and always wins; a generated one is better\nevidence than a required box filled with a full stop.",
            "maxLength": 500
          }
        }
      },
      "UpdatePayrollPayDateDto": {
        "type": "object",
        "properties": {
          "payDate": {
            "type": "string"
          }
        },
        "required": [
          "payDate"
        ]
      },
      "UpdatePayrollItemDto": {
        "type": "object",
        "properties": {
          "status": {
            "type": "object"
          }
        },
        "required": [
          "status"
        ]
      },
      "CreatePayrollAdjustmentDto": {
        "type": "object",
        "properties": {
          "amountCents": {
            "type": "number",
            "description": "MAY BE NEGATIVE (a deduction or clawback).\n\nNot accepted on a `LINE_REMOVAL`, which derives its cents from the line it\nremoves: a removal whose amount was typed could half-remove a line, and a\nline that is 90% removed is a number nobody can explain."
          },
          "reason": {
            "type": "string",
            "minLength": 1
          },
          "kind": {
            "type": "object",
            "description": "See {@link WRITABLE_ADJUSTMENT_KINDS}. Defaults to `AMOUNT`.",
            "enum": [
              "AMOUNT",
              "LINE_REMOVAL"
            ]
          },
          "workKey": {
            "type": "string",
            "description": "The unit of work this corrects. Omitted = the whole person's payout.",
            "minLength": 1
          }
        },
        "required": [
          "reason"
        ]
      },
      "BulkPayrollAdjustmentLineDto": {
        "type": "object",
        "properties": {
          "payrollItemId": {
            "type": "string",
            "description": "Which payee's line this row lands on. Verified against the run.",
            "minLength": 1
          },
          "workKey": {
            "type": "string",
            "description": "The unit of work being adjusted. REQUIRED here, unlike the single-line\nroute: a bulk request is by definition about specific lines, and a batch of\nperson-scoped lumps is the exact anti-pattern this feature replaces.",
            "minLength": 1
          },
          "amountCents": {
            "type": "number",
            "description": "MAY BE NEGATIVE. Not accepted on a `LINE_REMOVAL` — see the create DTO."
          },
          "reason": {
            "type": "string",
            "description": "Per-row, so a batch can carry one reason per line where the lines differ.\n Falls back to the request's `reason` when omitted.",
            "minLength": 1
          },
          "kind": {
            "type": "object",
            "enum": [
              "AMOUNT",
              "LINE_REMOVAL"
            ]
          }
        },
        "required": [
          "payrollItemId",
          "workKey"
        ]
      },
      "BulkPayrollAdjustmentDto": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "description": "The reason every row inherits unless it states its own. One of the two must\nbe present for every row — the service refuses a row that would end up with\nno reason, because a reason is the only thing that makes an adjustment\nlegible a month later.",
            "minLength": 1
          },
          "lines": {
            "description": "BOUNDED. Every row is a row in one transaction, and an unbounded batch is a\nlong-held lock on a run's items plus a request that can time out halfway\nthrough money. 500 is far above any real correction and far below anything\nthat endangers the transaction.",
            "minItems": 1,
            "maxItems": 500,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BulkPayrollAdjustmentLineDto"
            }
          }
        },
        "required": [
          "lines"
        ]
      },
      "UpdatePayrollAdjustmentDto": {
        "type": "object",
        "properties": {
          "amountCents": {
            "type": "number",
            "description": "MAY BE NEGATIVE. Refused on a `LINE_REMOVAL`: its cents are the exact\nnegative of what the line was priced at, and a typed figure could leave a\nline 90% removed."
          },
          "reason": {
            "type": "string",
            "minLength": 1
          }
        }
      },
      "UnfinalizeRunDto": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "minLength": 3
          },
          "confirmNotPaidFromExport": {
            "type": "boolean",
            "description": "Explicit acknowledgement that nobody has been paid from this run's export.\n\nRequired only when the run carries an export marker (`exportedAt` /\n`exportTarget`). Un-finalizing releases every work claim, so if a bookkeeper\nhas already paid from the downloaded CSV the next run prices and pays the same\nwork again — and CSV is the channel money actually leaves by here. A separate,\ndeliberately-named field rather than a flag folded into the reason string, so\nit cannot be sent by accident; it is recorded in the audit log as its own fact.\n\n`whitelist: true` strips unknown body fields silently, so this name must match\nthe client exactly — a typo would look like \"no override sent\" and 400."
          }
        },
        "required": [
          "reason"
        ]
      },
      "AttestVisitDto": {
        "type": "object",
        "properties": {
          "organizationMemberId": {
            "type": "string",
            "description": "The member whose pay line this attestation is for, when the caller knows it.\n\n`payrollOnSiteSecondsOverride` lives on the SHOOT, not on a (member, shoot)\npair, so it prices every technician assigned to that shoot and cannot be\nscoped per person without a schema change. What CAN be enforced is that the\nnamed member is actually one of the shoot's technicians — attesting hours\nagainst somebody who did not work it is always a mis-key, and it silently\nchanges what a different person is paid.\n\nTODO(blocked-by-schema): a per-member visit attestation needs a\n`(visitId, organizationMemberId)` override table mirroring\n`PayrollWorkAttestation`. Until then this field authorises the write; it does\nnot scope it."
          },
          "payrollOnSiteSecondsOverride": {
            "type": "number",
            "nullable": true,
            "description": "Attested on-site seconds, or an explicit `null` to CLEAR a previous\nattestation.\n\nThe service used to write `dto.payrollOnSiteSecondsOverride ?? undefined`,\nand Prisma reads `undefined` as \"leave this column alone\" — so once an\noverride existed, no request could remove it. A mis-keyed 8-hour attestation\noutranks the booked window on every future compute, permanently. `null` now\nclears it, and the audit entry records the value it replaced.",
            "minimum": 0,
            "maximum": 43200
          }
        }
      },
      "AttestProjectWorkDto": {
        "type": "object",
        "properties": {
          "organizationMemberId": {
            "type": "string"
          },
          "onSiteSeconds": {
            "type": "number",
            "description": "Attested on-site seconds. Bounded by the same 12-hour plausibility ceiling\n the GPS band uses, so an attestation cannot assert something a measured\n session would have been rejected for — and by the SAME constant the\n visit-level route uses, which had no ceiling at all.",
            "minimum": 0,
            "maximum": 43200
          }
        },
        "required": [
          "organizationMemberId",
          "onSiteSeconds"
        ]
      },
      "ImportPayStructureDto": {
        "type": "object",
        "properties": {
          "csv": {
            "type": "string"
          },
          "preview": {
            "type": "boolean"
          }
        },
        "required": [
          "csv"
        ]
      },
      "SetQuickBooksMappingDto": {
        "type": "object",
        "properties": {
          "vendorMapping": {
            "type": "object",
            "description": "`{ [OrganizationMember.id]: quickbooksVendorId }` — CONTRACTORS ONLY.\nIntuit accepts only a Vendor as a Bill payee, so this is the contractor\nrail and an employee must never appear in it."
          },
          "employeeMapping": {
            "type": "object",
            "description": "`{ [OrganizationMember.id]: quickbooksEmployeeId }` — EMPLOYEES ONLY.\nThe SAME key as `vendorMapping` (the MEMBERSHIP id, not the user id),\nbecause `exportRun` looks both up as `map[item.organizationMemberId]`.\n\n⚠ `class-validator` runs with `whitelist: true`, so a key this DTO does not\ndeclare is silently STRIPPED — no 400, no error, just a mapping that never\nsaves. This field must stay declared for the employee rail to work at all."
          },
          "accountMapping": {
            "type": "object",
            "description": "`{ wages, reimbursement, wagesPayable }`. The first two are expense\naccounts; `wagesPayable` is a liability account, and it is validated\nserver-side against the live QuickBooks chart of accounts — Accounts\nPayable is refused, because QuickBooks requires a vendor on every A/P line\nand an employee is not a vendor."
          }
        }
      },
      "ExportRunDto": {
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "enum": [
              "QUICKBOOKS"
            ]
          }
        },
        "required": [
          "provider"
        ]
      },
      "QuickBooksCallbackDto": {
        "type": "object",
        "properties": {
          "code": {
            "type": "string"
          },
          "realmId": {
            "type": "string"
          },
          "state": {
            "type": "string"
          }
        },
        "required": [
          "code",
          "realmId",
          "state"
        ]
      },
      "UpsertHomeBaseOverrideDto": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string"
          },
          "routeDate": {
            "type": "string"
          },
          "projectVisitId": {
            "type": "string"
          },
          "addressLine1": {
            "type": "string"
          },
          "addressLine2": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "returnsHere": {
            "type": "boolean",
            "description": "Does the technician also RETURN here at the end of the day?\n\nMUST be declared: `class-validator` runs with `whitelist: true`, so an\nundeclared field is stripped silently — the request would look like it\nsucceeded and the return leg would keep going to the profile home base,\nwhich is a pay difference nobody would see until a run."
          },
          "note": {
            "type": "string",
            "description": "Why. Shown to the technician, so it is bounded rather than free-form.",
            "maxLength": 500
          }
        },
        "required": [
          "userId",
          "routeDate",
          "lat",
          "lng"
        ]
      },
      "CreatePayrollManualItemDto": {
        "type": "object",
        "properties": {
          "organizationMemberId": {
            "type": "string",
            "description": "The `OrganizationMember.id` who is owed. Pay is owed to a MEMBERSHIP, not to\n a user account that may belong to several orgs.",
            "minLength": 1
          },
          "title": {
            "type": "string",
            "description": "What the line says on the pay run. Also becomes the `reason` on the\n`PayrollAdjustment` it is materialised as, so the same words appear on the\nitem, in the CSV and on the QuickBooks Bill line.",
            "minLength": 1,
            "maxLength": 200
          },
          "amountCents": {
            "type": "number",
            "description": "May be NEGATIVE — a deduction (equipment recovery, a clawback) is an ad-hoc\npayable line too.\n\nZERO IS REJECTED. A zero line is a mis-key, not a decision: it would sit in\nthe outstanding pool, be collected by a run, and materialise as a CA$0.00\nadjustment that means nothing to whoever reads it later."
          },
          "projectId": {
            "type": "string",
            "description": "Optional job this line is about."
          },
          "notes": {
            "type": "string",
            "maxLength": 2000
          },
          "incurredAt": {
            "type": "string",
            "description": "WHEN THIS BECAME PAYABLE. Decides which run's period collects it, and is the\ndate the Pay Run Items table shows. Defaults to now, so a line entered for\ntoday needs no date; set it explicitly to book a line into the period the work\nactually belongs to."
          }
        },
        "required": [
          "organizationMemberId",
          "title",
          "amountCents"
        ]
      },
      "UpdatePayrollManualItemDto": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "amountCents": {
            "type": "number",
            "description": "Zero is rejected for the same reason as on create."
          },
          "projectId": {
            "type": "string",
            "nullable": true,
            "description": "Send `null` to unlink the job."
          },
          "notes": {
            "type": "string",
            "nullable": true,
            "maxLength": 2000
          },
          "incurredAt": {
            "type": "string"
          }
        }
      },
      "SetMediaRateDto": {
        "type": "object",
        "properties": {
          "mediaType": {
            "type": "object",
            "description": "The EXISTING `MediaType` enum, MINUS the non-deliverable storage buckets.\nNothing here privileges FLOORPLAN or VIRTUAL_TOUR — any type that somebody\ncan actually COMPLETE may carry a rate.\n\nDOCUMENT is refused rather than accepted-and-ignored. A rate is only ever\npaid on a completion, and a document is filed rather than completed (see\n`SLA_NON_DELIVERABLE_MEDIA_TYPES`), so accepting one would write a real\n`OrgMediaRateVersion` row that reads back as configured pay and can never\nproduce a cent. `@IsIn` and not `@IsEnum`, because `whitelist: true` strips\nunknown KEYS but does not narrow a valid enum VALUE.",
            "enum": [
              "PHOTO",
              "VIDEO",
              "FLOORPLAN",
              "VIRTUAL_TOUR",
              "PROPERTY_WEBSITE",
              "BROCHURE"
            ]
          },
          "amountCents": {
            "type": "number",
            "description": "Integer cents. `0` is ACCEPTED and means a DELIBERATE zero — \"we do not\npiece-rate this type\" — which is honoured silently. It is NOT the same as\nleaving the type unrated: an unrated type has no version row at all and\nraises `RATE_NOT_CONFIGURED` on a completion instead of paying CA$0.00.\nNever write 0 to mean \"unset\"; the two are not recoverable from each other.",
            "minimum": 0
          },
          "currency": {
            "type": "string",
            "description": "ISO-4217. Defaults to the org's own currency. A rate in a currency the run\n does not pay in is REFUSED at compute time, never converted.",
            "minLength": 3,
            "maxLength": 3
          },
          "effectiveFrom": {
            "type": "string",
            "description": "When this rate takes effect. Omit for NOW (the ordinary forward change).\n\nA PAST instant is a backdated correction and requires `reason`. A FUTURE\ninstant is refused — see the service."
          },
          "reason": {
            "type": "string",
            "description": "Why the past is being corrected. REQUIRED on a backdated write (enforced in\nthe service, because it depends on `effectiveFrom` vs now). This is money.",
            "maxLength": 500
          }
        },
        "required": [
          "mediaType",
          "amountCents"
        ]
      },
      "CreateIncentiveRuleDto": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "description": "Which predicate decides whether the bonus is earned. `SLA_BEAT` today.\n\nValidated against the values this BUILD can actually evaluate, not against a\nfree string: a rule naming a predicate the running code does not implement\nwould sit in the table paying nothing, or — worse, if the check were absent\nfrom the engine too — paying unconditionally.",
            "enum": [
              "SLA_BEAT"
            ]
          },
          "mediaType": {
            "type": "object",
            "description": "Restrict to one media type, or omit for every type.\n\nSame narrowing as `SetMediaRateDto`: an incentive is a bonus paid ON a\ncompletion, so a non-deliverable storage bucket (DOCUMENT) can never be its\ntarget and is refused rather than stored as a rule that never fires.",
            "enum": [
              "PHOTO",
              "VIDEO",
              "FLOORPLAN",
              "VIRTUAL_TOUR",
              "PROPERTY_WEBSITE",
              "BROCHURE"
            ]
          },
          "amountMode": {
            "type": "string",
            "description": "FLAT — pay `amountCents`. PERCENT — pay `percentBps` of the piece rate.",
            "enum": [
              "FLAT",
              "PERCENT"
            ]
          },
          "amountCents": {
            "type": "number",
            "description": "Integer cents, for FLAT. Required for FLAT and rejected as zero — a bonus\n that pays nothing is a rule that does nothing.",
            "minimum": 0
          },
          "percentBps": {
            "type": "number",
            "description": "Basis points of the piece rate, for PERCENT. 10000 = 100%.\n\nAn INTEGER and not a float percentage, so the bonus can never arrive as\n`12.5000000001` and round differently on two computes of the same run.\nCapped at 10000: a bonus larger than the work itself is a mis-key.",
            "minimum": 0,
            "maximum": 10000
          },
          "currency": {
            "type": "string",
            "description": "ISO-4217 for a FLAT amount. Defaults to the org's own currency.",
            "minLength": 3,
            "maxLength": 3
          },
          "label": {
            "type": "string",
            "description": "What the pay run calls this line. Shown on the review screen beside the\n money, so a reviewer sees WHICH rule paid rather than a bare figure.",
            "maxLength": 120
          },
          "config": {
            "type": "object",
            "description": "Kind-specific parameters. `SLA_BEAT` needs none; declared so a future kind\n can carry its own without a migration or a DTO change."
          },
          "effectiveFrom": {
            "type": "string",
            "description": "── WHEN THIS BONUS STARTS APPLYING. Omit for NOW. ─────────────────────────\n\nA PAST instant BACKDATES the rule over work that is already finished, and\nrequires `reason`. It is the same shape `SetMediaRateDto.effectiveFrom`\nalready has, and it exists for the same reason: an org that decides in\nSeptember to pay an SLA bonus for August has two ways to do it, and only\none of them produces figures the engine can reproduce.\n\nThis route used to refuse it outright, on the argument that a bonus minted\nwith a past date \"retroactively awards money on completions that have\nalready been reviewed\". Both halves of that are still true and neither is\nan argument for refusing:\n\n • A FINALIZED or EXPORTED run cannot be recomputed at all\n   (`PayrollRunService.recompute` refuses before reading a candidate), so\n   no money any run has committed to can move.\n • On a run that CAN recompute, the alternative was a hand-typed\n   `PayrollManualItem` — a number nobody can re-derive, that no rate ladder\n   explains and that a second reviewer cannot check. Backdating the rule\n   reprices through the same `evaluateIncentive` the engine always uses.\n\nWhat the refusal was actually protecting was the reader's ability to tell a\nbonus that was in force from one filled in later. That is now carried on\nevery award (`IncentiveAward.provenance`, from `createdAt` vs\n`effectiveFrom`) and on every line (`basis.provenance`), so the protection\nsurvives without the prohibition — and an EMPLOYEE's line still refuses to\nmove for a backdated rule, whatever this field says.\n\nA FUTURE instant is refused, exactly as it is for a rate."
          },
          "reason": {
            "type": "string",
            "description": "Why a bonus is being applied to work that is already done. REQUIRED on a\nbackdated write (enforced in the service, because it depends on\n`effectiveFrom` vs now). Recorded on the audit entry. This is money.",
            "maxLength": 500
          }
        },
        "required": [
          "kind",
          "amountMode",
          "label"
        ]
      },
      "ListDeliverableTonesDto": {
        "type": "object",
        "properties": {
          "projectIds": {
            "description": "Project ids to resolve lanes for.\n\n`ArrayMaxSize(200)` is THE COST CEILING and it is deliberately equal to the\nproject list's own maximum page size: this endpoint runs three id-bounded\nqueries, so its worst case is pinned to one page of the board rather than\nto whatever a caller feels like sending.\n\nUnknown ids, and ids the caller has no staff standing on, are simply ABSENT\nfrom the response — never 403. A 403 on a mixed page would fail the whole\nboard because of one card, and a per-id 403 would be an enumeration oracle.",
            "minItems": 1,
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "projectIds"
        ]
      },
      "MarkDeliverableDto": {
        "type": "object",
        "properties": {
          "mediaType": {
            "type": "string",
            "enum": [
              "PHOTO",
              "VIDEO",
              "FLOORPLAN",
              "DOCUMENT",
              "VIRTUAL_TOUR",
              "PROPERTY_WEBSITE",
              "BROCHURE"
            ],
            "description": "The media type being marked complete. Must be one the project ordered."
          }
        },
        "required": [
          "mediaType"
        ]
      },
      "DeliverableAssigneeDto": {
        "type": "object",
        "properties": {
          "mediaType": {
            "type": "string",
            "enum": [
              "PHOTO",
              "VIDEO",
              "FLOORPLAN",
              "DOCUMENT",
              "VIRTUAL_TOUR",
              "PROPERTY_WEBSITE",
              "BROCHURE"
            ],
            "description": "Which media type of this project the person is on."
          },
          "userId": {
            "type": "string",
            "minLength": 1,
            "description": "User id of the assignee (an org member)."
          }
        },
        "required": [
          "mediaType",
          "userId"
        ]
      },
      "DeliverableRoutingLockDto": {
        "type": "object",
        "properties": {
          "mediaType": {
            "type": "string",
            "enum": [
              "PHOTO",
              "VIDEO",
              "FLOORPLAN",
              "DOCUMENT",
              "VIRTUAL_TOUR",
              "PROPERTY_WEBSITE",
              "BROCHURE"
            ],
            "description": "Which media type to re-enable automatic routing for."
          }
        },
        "required": [
          "mediaType"
        ]
      },
      "OverrideStageOutcomeDto": {
        "type": "object",
        "properties": {
          "outcome": {
            "type": "string",
            "enum": [
              "ON_TIME",
              "LATE",
              "EXCUSED"
            ],
            "nullable": true,
            "description": "ON_TIME | LATE | EXCUSED, or null to clear. EXCUSED removes the stage from BOTH sides of every rate."
          },
          "note": {
            "type": "string",
            "nullable": true,
            "description": "Why. Recorded on the row AND in the audit log — an override with no reason is unreviewable later. null clears it.",
            "maxLength": 1000
          }
        }
      },
      "ToggleAnalyticsSubscriptionDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 320,
            "format": "email"
          },
          "enabled": {
            "type": "boolean"
          }
        },
        "required": [
          "email",
          "enabled"
        ]
      }
    }
  }
}
