diff --git a/.gitignore b/.gitignore index 7f249df..a64a4d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ node_modules/ dist/ .npm-cache/ -# client generated from openapi.json — regenerated with `pnpm generate` +# clients generated from the specs — regenerated with `pnpm generate` src/client/ +src/admin-client/ *.log .DS_Store .env diff --git a/CHANGELOG.md b/CHANGELOG.md index b08747a..56ea2c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,44 @@ versioning follows [SemVer](https://semver.org/). ## [Unreleased] +## [0.4.0] - 2026-06-29 + +### Added +- **Write layer (B2B v1):** every resource now has `create`/`update`/`delete` + plus resource actions — `events publish`, `sales cancel`/`refund`, + `staff create`/`set-role`. Bodies via `--data` (inline JSON or `@file.json`); + `delete` confirms unless `--yes`. +- `event-dates` command group for the nested `/events/{id}/dates` resource + (`list`/`create `, `update`/`delete `). +- **Admin writes:** `workspaces create`/`update`/`suspend`/`restore`, + `users update`, `plans create`/`update`, `feature-flags set `, + `impersonate`/`impersonate-stop`. +- **`--csv`** flag on every list command (B2B + admin) — CSV on stdout for + spreadsheets/accounting, columns matching the table view. +- `lib/input.ts`: `parseData` (`--data` inline/`@file` JSON) and an interactive + `confirm` for destructive ops (auto-aborts on non-TTY stdin). +- `toCsv` (RFC 4180 quoting) in `lib/output.ts`. + +## [0.3.0] - 2026-06-29 + +### Added +- Superadmin commands (`ft admin …`) against the separate `/api/admin` contract + (Admin API v1.0.0): `me`, `workspaces`, `users`, `plans`, `feature-flags`, + `audit-log` (read-only first pass). +- Second generated client (`src/admin-client/`) via `openapi-ts.admin.config.ts`, + with its own `client` singleton and auth. +- `configureAdminClient` using a SUPER_ADMIN better-auth session + (`FT_ADMIN_SESSION`) — the admin contract issues no API key. +- `sync-openapi:admin` script (target `FT_ADMIN_OPENAPI_URL`); `generate` now + builds both the B2B and admin clients. + +## [0.2.0] - 2026-06-29 + +### Added +- CFO financial reconciliation against B2B contract v1.1.0: + `ft reports reconciliation` and `ft reports export reconciliation` — cross + Mercado Pago payment, sale and Siigo invoice with a `match_status` per sale. + ## [0.1.1] - 2026-06-25 ### Changed diff --git a/admin-openapi.json b/admin-openapi.json new file mode 100644 index 0000000..264b278 --- /dev/null +++ b/admin-openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"FreeTicket Admin API","version":"1.0.0","description":"API REST de superadmin de FreeTicket (cross-tenant). Autenticación por sesión de plataforma (staff con rol SUPER_ADMIN) — NO usa API key de tenant. Aislada del contrato B2B /api/v1."},"servers":[{"url":"https://admin.appfreeticket.com/api/admin"}],"security":[{"session":[]}],"paths":{"/me":{"get":{"operationId":"getMe","tags":["me"],"summary":"Identidad del superadmin autenticado","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminMe"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/workspaces":{"get":{"operationId":"getWorkspaces","tags":["workspaces"],"summary":"Listar tenants (cross-tenant)","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"parameters":[{"name":"status","in":"query","required":false,"description":"active(default)|suspended|deleted|all","schema":{"type":"string"}},{"name":"q","in":"query","required":false,"description":"Búsqueda por nombre o slug","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdminWorkspace"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"postWorkspaces","tags":["workspaces"],"summary":"Crear tenant","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWorkspaceCreate"}}}},"responses":{"201":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminWorkspace"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/workspaces/{id}":{"get":{"operationId":"getWorkspacesId","tags":["workspaces"],"summary":"Detalle de un tenant","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminWorkspace"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"patchWorkspacesId","tags":["workspaces"],"summary":"Actualizar tenant","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminWorkspaceUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminWorkspace"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/workspaces/{id}/suspend":{"post":{"operationId":"postWorkspacesIdSuspend","tags":["workspaces"],"summary":"Suspender tenant (corta acceso B2B)","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminWorkspace"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/workspaces/{id}/restore":{"post":{"operationId":"postWorkspacesIdRestore","tags":["workspaces"],"summary":"Reactivar tenant suspendido","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminWorkspace"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/users":{"get":{"operationId":"getUsers","tags":["users"],"summary":"Listar usuarios cross-tenant","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"parameters":[{"name":"q","in":"query","required":false,"description":"Búsqueda por nombre o email","schema":{"type":"string"}},{"name":"role","in":"query","required":false,"description":"SUPER_ADMIN|ADMIN|STAFF|VIEWER|MINCULTURA","schema":{"type":"string"}},{"name":"workspaceId","in":"query","required":false,"description":"Filtra por membresía a un workspace","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdminUser"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/users/{id}":{"get":{"operationId":"getUsersId","tags":["users"],"summary":"Detalle de usuario (incluye workspaces)","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminUser"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"patchUsersId","tags":["users"],"summary":"Actualizar rol global / baneo","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminUser"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/impersonate":{"post":{"operationId":"postImpersonate","tags":["impersonation"],"summary":"Emitir token de impersonation acotado","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminImpersonate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminImpersonation"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/impersonate/stop":{"post":{"operationId":"postImpersonateStop","tags":["impersonation"],"summary":"Terminar impersonation","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/platform-plans":{"get":{"operationId":"getPlatformPlans","tags":["platform-plans"],"summary":"Listar planes de plataforma","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdminPlatformPlan"}}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"postPlatformPlans","tags":["platform-plans"],"summary":"Crear plan de plataforma","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminPlatformPlanCreate"}}}},"responses":{"201":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminPlatformPlan"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/platform-plans/{id}":{"get":{"operationId":"getPlatformPlansId","tags":["platform-plans"],"summary":"Detalle de plan (límites, pricing, features)","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminPlatformPlan"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"patchPlatformPlansId","tags":["platform-plans"],"summary":"Editar límites/pricing/features de un plan","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminPlatformPlanUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminPlatformPlan"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/feature-flags":{"get":{"operationId":"getFeatureFlags","tags":["feature-flags"],"summary":"Listar feature flags","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"parameters":[{"name":"key","in":"query","required":false,"schema":{"type":"string"}},{"name":"scope","in":"query","required":false,"description":"global|plan|workspace","schema":{"type":"string"}},{"name":"scopeId","in":"query","required":false,"description":"plan.slug u organization.id","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdminFeatureFlag"}}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/feature-flags/{key}":{"put":{"operationId":"putFeatureFlagsKey","tags":["feature-flags"],"summary":"Setear un feature flag por scope","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminFeatureFlagSet"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdminFeatureFlag"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/audit-log":{"get":{"operationId":"getAuditLog","tags":["audit-log"],"summary":"Registro append-only de acciones de superadmin","description":"Requiere sesión de plataforma con rol `SUPER_ADMIN`.","security":[{"session":[]}],"parameters":[{"name":"actorId","in":"query","required":false,"schema":{"type":"string"}},{"name":"action","in":"query","required":false,"schema":{"type":"string"}},{"name":"targetType","in":"query","required":false,"schema":{"type":"string"}},{"name":"from","in":"query","required":false,"description":"ISO 8601","schema":{"type":"string"}},{"name":"to","in":"query","required":false,"description":"ISO 8601","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AdminAuditEntry"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o recurso no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera de alcance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"AdminMe":{"type":"object","properties":{"userId":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"role":{"type":"string","enum":["SUPER_ADMIN","ADMIN","STAFF","VIEWER","MINCULTURA"]}},"required":["userId","name","email","role"],"additionalProperties":false},"FeatureFlagScope":{"type":"string","enum":["global","plan","workspace"]},"Page":{"type":"object","properties":{"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}]},"hasMore":{"type":"boolean"}},"required":["nextCursor","hasMore"],"additionalProperties":false},"AdminWorkspace":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"type":{"type":"string","enum":["ARTIST","VENUE","ORGANIZER"]},"country":{"type":"string"},"isPublished":{"type":"boolean"},"suspended":{"type":"boolean"},"suspendedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"deletedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["id","name","slug","type","country","isPublished","suspended","suspendedAt","deletedAt","createdAt"],"additionalProperties":false},"AdminWorkspaceCreate":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1,"pattern":"^[a-z0-9-]+$"},"type":{"default":"ORGANIZER","type":"string","enum":["ARTIST","VENUE","ORGANIZER"]},"country":{"default":"CO","type":"string"},"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["name","slug","type","country"],"additionalProperties":false},"AdminWorkspaceUpdate":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1,"pattern":"^[a-z0-9-]+$"},"type":{"type":"string","enum":["ARTIST","VENUE","ORGANIZER"]},"isPublished":{"type":"boolean"}},"additionalProperties":false},"AdminUserWorkspace":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"isOwner":{"type":"boolean"}},"required":["id","name","slug","isOwner"],"additionalProperties":false},"AdminUser":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"role":{"type":"string","enum":["SUPER_ADMIN","ADMIN","STAFF","VIEWER","MINCULTURA"]},"banned":{"type":"boolean"},"bannedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"homeOrganizationId":{"anyOf":[{"type":"string"},{"type":"null"}]},"workspaces":{"type":"array","items":{"$ref":"#/components/schemas/AdminUserWorkspace"}},"createdAt":{"type":"string"}},"required":["id","name","email","role","banned","bannedAt","homeOrganizationId","workspaces","createdAt"],"additionalProperties":false},"AdminUserUpdate":{"type":"object","properties":{"role":{"type":"string","enum":["SUPER_ADMIN","ADMIN","STAFF","VIEWER","MINCULTURA"]},"banned":{"type":"boolean"}},"additionalProperties":false},"AdminImpersonate":{"type":"object","properties":{"targetUserId":{"type":"string","minLength":1},"workspaceId":{"type":"string","minLength":1}},"additionalProperties":false},"AdminImpersonation":{"type":"object","properties":{"token":{"type":"string"},"expiresAt":{"type":"string"},"actorUserId":{"type":"string"},"targetUserId":{"type":"string"},"targetOrgId":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["token","expiresAt","actorUserId","targetUserId","targetOrgId"],"additionalProperties":false},"AdminPlatformPlan":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"priceMonthly":{"type":"number"},"priceYearly":{"type":"number"},"priceBiannual":{"anyOf":[{"type":"number"},{"type":"null"}]},"maxEvents":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"isActive":{"type":"boolean"},"sortOrder":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"features":{"type":"object","properties":{"memberships":{"type":"boolean"},"content":{"type":"boolean"},"fanDatabase":{"type":"boolean"},"directMessages":{"type":"boolean"},"streaming":{"type":"boolean"},"customLanding":{"type":"boolean"},"dataExport":{"type":"boolean"},"seatMaps":{"type":"boolean"},"customDomain":{"type":"boolean"},"analytics":{"type":"boolean"}},"required":["memberships","content","fanDatabase","directMessages","streaming","customLanding","dataExport","seatMaps","customDomain","analytics"],"additionalProperties":false},"createdAt":{"type":"string"}},"required":["id","name","slug","priceMonthly","priceYearly","priceBiannual","maxEvents","isActive","sortOrder","features","createdAt"],"additionalProperties":false},"AdminPlatformPlanCreate":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1,"pattern":"^[a-z0-9-]+$"},"priceMonthly":{"default":0,"type":"number","minimum":0},"priceYearly":{"default":0,"type":"number","minimum":0},"priceBiannual":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}]},"isActive":{"default":true,"type":"boolean"},"memberships":{"type":"boolean"},"content":{"type":"boolean"},"fanDatabase":{"type":"boolean"},"directMessages":{"type":"boolean"},"streaming":{"type":"boolean"},"customLanding":{"type":"boolean"},"dataExport":{"type":"boolean"},"seatMaps":{"type":"boolean"},"customDomain":{"type":"boolean"},"analytics":{"type":"boolean"},"maxEvents":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]}},"required":["name","slug","priceMonthly","priceYearly","isActive"],"additionalProperties":false},"AdminPlatformPlanUpdate":{"type":"object","properties":{"name":{"type":"string","minLength":1},"priceMonthly":{"type":"number","minimum":0},"priceYearly":{"type":"number","minimum":0},"priceBiannual":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}]},"isActive":{"type":"boolean"},"sortOrder":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"memberships":{"type":"boolean"},"content":{"type":"boolean"},"fanDatabase":{"type":"boolean"},"directMessages":{"type":"boolean"},"streaming":{"type":"boolean"},"customLanding":{"type":"boolean"},"dataExport":{"type":"boolean"},"seatMaps":{"type":"boolean"},"customDomain":{"type":"boolean"},"analytics":{"type":"boolean"},"maxEvents":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]}},"additionalProperties":false},"AdminFeatureFlag":{"type":"object","properties":{"key":{"type":"string"},"scope":{"$ref":"#/components/schemas/FeatureFlagScope"},"scopeId":{"anyOf":[{"type":"string"},{"type":"null"}]},"enabled":{"type":"boolean"},"updatedBy":{"anyOf":[{"type":"string"},{"type":"null"}]},"updatedAt":{"type":"string"}},"required":["key","scope","scopeId","enabled","updatedBy","updatedAt"],"additionalProperties":false},"AdminFeatureFlagSet":{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/FeatureFlagScope"},"scopeId":{"type":"string","minLength":1},"enabled":{"type":"boolean"}},"required":["scope","enabled"],"additionalProperties":false},"AdminAuditEntry":{"type":"object","properties":{"id":{"type":"string"},"actorUserId":{"type":"string"},"action":{"type":"string"},"targetType":{"type":"string"},"targetId":{"type":"string"},"before":{"anyOf":[{},{"type":"null"}]},"after":{"anyOf":[{},{"type":"null"}]},"ipAddress":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["id","actorUserId","action","targetType","targetId","before","after","ipAddress","createdAt"],"additionalProperties":false}},"securitySchemes":{"session":{"type":"apiKey","in":"cookie","name":"better-auth.session_token","description":"Cookie de sesión de plataforma (better-auth staff). El portador debe tener rol SUPER_ADMIN. No se emite API key para este contrato."}}}} \ No newline at end of file diff --git a/openapi-ts.admin.config.ts b/openapi-ts.admin.config.ts new file mode 100644 index 0000000..dd3c696 --- /dev/null +++ b/openapi-ts.admin.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "@hey-api/openapi-ts"; + +// Cliente del contrato superadmin (/api/admin). Generado — nunca a mano. +// Output separado del B2B: su propio `client` singleton => auth independiente +// (sesión SUPER_ADMIN, no API key). Ver src/lib/api.ts > configureAdminClient. +export default defineConfig({ + input: "admin-openapi.json", + output: "src/admin-client", + plugins: ["@hey-api/client-fetch"], +}); diff --git a/openapi-ts.config.ts b/openapi-ts.config.ts index 9b8be72..76bf938 100644 --- a/openapi-ts.config.ts +++ b/openapi-ts.config.ts @@ -1,7 +1,9 @@ import { defineConfig } from "@hey-api/openapi-ts"; -// Genera el cliente tipado desde el contrato commiteado. +// Genera el cliente B2B v1 desde el contrato commiteado. // src/client/ es generado — nunca se edita a mano (ver .gitignore). +// El cliente admin se genera aparte (openapi-ts.admin.config.ts), con su +// propio `client` singleton, para que la auth admin no se mezcle con la B2B. export default defineConfig({ input: "openapi.json", output: "src/client", diff --git a/openapi.json b/openapi.json index ecefaac..af351ef 100644 --- a/openapi.json +++ b/openapi.json @@ -1 +1,4673 @@ -{"openapi":"3.1.0","info":{"title":"FreeTicket B2B API","version":"1.0.0","description":"API REST B2B de FreeTicket para integraciones y el CLI. Autenticación por API key (header `Authorization: Bearer ` o `x-api-key`); el workspace activo se selecciona con el header `X-Workspace-Id`."},"servers":[{"url":"http://localhost:3000/api/v1"}],"security":[{"apiKey":[]}],"paths":{"/me":{"get":{"operationId":"getMe","tags":["me"],"summary":"Usuario autenticado, rol y workspaces accesibles","description":"Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Me"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/events":{"get":{"operationId":"getEvents","tags":["events"],"summary":"Listar eventos del workspace","description":"Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"limit","in":"query","required":false,"description":"1-100, default 20","schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"description":"id del último resultado de la página previa","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"postEvents","tags":["events"],"summary":"Crear evento (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventCreate"}}}},"responses":{"201":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Event"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/events/{id}":{"get":{"operationId":"getEventsId","tags":["events"],"summary":"Obtener un evento","description":"Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Event"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"patchEventsId","tags":["events"],"summary":"Actualizar evento (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Event"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteEventsId","tags":["events"],"summary":"Eliminar (soft) evento (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/events/{id}/publish":{"post":{"operationId":"postEventsIdPublish","tags":["events"],"summary":"Publicar evento (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Event"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/events/{id}/dates":{"get":{"operationId":"getEventsIdDates","tags":["events"],"summary":"Listar fechas de un evento","description":"Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/EventDate"}}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"postEventsIdDates","tags":["events"],"summary":"Agregar fecha a un evento (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EventDate"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/events/{id}/dates/{dateId}":{"patch":{"operationId":"patchEventsIdDatesDateId","tags":["events"],"summary":"Actualizar fecha (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"dateId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/EventDate"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteEventsIdDatesDateId","tags":["events"],"summary":"Eliminar fecha (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"dateId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/ticket-types":{"get":{"operationId":"getTicketTypes","tags":["ticket-types"],"summary":"Listar tipos de ticket del workspace","description":"Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"eventDateId","in":"query","required":false,"description":"filtra por fecha de evento","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/TicketType"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"postTicketTypes","tags":["ticket-types"],"summary":"Crear tipo de ticket (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketTypeCreate"}}}},"responses":{"201":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TicketType"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/ticket-types/{id}":{"get":{"operationId":"getTicketTypesId","tags":["ticket-types"],"summary":"Obtener un tipo de ticket","description":"Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TicketType"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"patchTicketTypesId","tags":["ticket-types"],"summary":"Actualizar tipo de ticket (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TicketType"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteTicketTypesId","tags":["ticket-types"],"summary":"Eliminar (soft) tipo de ticket (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sales":{"get":{"operationId":"getSales","tags":["sales"],"summary":"Listar ventas del workspace","description":"Rol mínimo: `STAFF`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"status","in":"query","required":false,"description":"PENDING|CONFIRMED|ABANDONED|CANCELLED|REFUNDED","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Sale"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sales/{id}":{"get":{"operationId":"getSalesId","tags":["sales"],"summary":"Detalle de una venta con sus ítems","description":"Rol mínimo: `STAFF`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SaleDetail"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sales/{id}/cancel":{"post":{"operationId":"postSalesIdCancel","tags":["sales"],"summary":"Cancelar una venta (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Sale"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/sales/{id}/refund":{"post":{"operationId":"postSalesIdRefund","tags":["sales"],"summary":"Reembolsar una venta (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Sale"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/membership-plans":{"get":{"operationId":"getMembershipPlans","tags":["membership-plans"],"summary":"Listar planes de membresía del workspace","description":"Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MembershipPlan"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"postMembershipPlans","tags":["membership-plans"],"summary":"Crear plan de membresía (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MembershipPlanCreate"}}}},"responses":{"201":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MembershipPlan"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/membership-plans/{id}":{"get":{"operationId":"getMembershipPlansId","tags":["membership-plans"],"summary":"Obtener un plan de membresía","description":"Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MembershipPlan"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"patchMembershipPlansId","tags":["membership-plans"],"summary":"Actualizar plan (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MembershipPlan"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteMembershipPlansId","tags":["membership-plans"],"summary":"Eliminar (soft) plan (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/venues":{"get":{"operationId":"getVenues","tags":["venues"],"summary":"Listar venues del workspace","description":"Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Venue"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"postVenues","tags":["venues"],"summary":"Crear venue (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VenueCreate"}}}},"responses":{"201":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Venue"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/venues/{id}":{"get":{"operationId":"getVenuesId","tags":["venues"],"summary":"Obtener un venue","description":"Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Venue"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"patchVenuesId","tags":["venues"],"summary":"Actualizar venue (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Venue"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deleteVenuesId","tags":["venues"],"summary":"Eliminar (soft) venue (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/staff":{"get":{"operationId":"getStaff","tags":["staff"],"summary":"Listar staff con acceso al workspace","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"string"}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/StaffUser"}},"page":{"$ref":"#/components/schemas/Page"}},"required":["data","page"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"postStaff","tags":["staff"],"summary":"Crear usuario staff (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StaffCreate"}}}},"responses":{"201":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/StaffUser"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/staff/{id}/role":{"patch":{"operationId":"patchStaffIdRole","tags":["staff"],"summary":"Cambiar rol de un usuario staff (fase 2)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/StaffUser"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/reports/summary":{"get":{"operationId":"getReportsSummary","tags":["reports"],"summary":"KPIs del workspace (revenue, tickets, ventas, membresías)","description":"Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"parameters":[{"name":"period","in":"query","required":false,"description":"7d | 30d (default) | 90d | 1y","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ReportSummary"}},"required":["data"]}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/reports/exports/buyers":{"get":{"operationId":"getReportsExportsBuyers","tags":["reports"],"summary":"Export de compradores (máx 5000)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/reports/exports/subscribers":{"get":{"operationId":"getReportsExportsSubscribers","tags":["reports"],"summary":"Export de suscriptores (máx 5000)","description":"Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.","security":[{"apiKey":[]}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Credencial inválida o ausente.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Rol insuficiente o workspace no accesible.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Recurso inexistente o fuera del tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Validación del cuerpo/parámetros.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"Page":{"type":"object","properties":{"nextCursor":{"anyOf":[{"type":"string"},{"type":"null"}]},"hasMore":{"type":"boolean"}},"required":["nextCursor","hasMore"],"additionalProperties":false},"Workspace":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"],"additionalProperties":false},"Me":{"type":"object","properties":{"userId":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"role":{"type":"string","enum":["SUPER_ADMIN","ADMIN","STAFF","VIEWER"]},"activeWorkspaceId":{"type":"string"},"workspaces":{"type":"array","items":{"$ref":"#/components/schemas/Workspace"}}},"required":["userId","name","email","role","activeWorkspaceId","workspaces"],"additionalProperties":false},"Event":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"status":{"type":"string","enum":["DRAFT","PUBLISHED","SOLD_OUT","CANCELLED","COMPLETED"]},"coverImageUrl":{"anyOf":[{"type":"string"},{"type":"null"}]},"organizationId":{"type":"string"},"venueId":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","name","slug","description","status","coverImageUrl","organizationId","venueId","createdAt","updatedAt"],"additionalProperties":false},"EventDate":{"type":"object","properties":{"id":{"type":"string"},"eventId":{"type":"string"},"label":{"anyOf":[{"type":"string"},{"type":"null"}]},"startsAt":{"type":"string"},"endsAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"doorsOpenAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"timezone":{"type":"string"},"venueId":{"anyOf":[{"type":"string"},{"type":"null"}]},"status":{"type":"string","enum":["DRAFT","PUBLISHED","SOLD_OUT","CANCELLED","COMPLETED"]},"streamType":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","eventId","label","startsAt","endsAt","doorsOpenAt","timezone","venueId","status","streamType","createdAt"],"additionalProperties":false},"EventCreate":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1,"pattern":"^[a-z0-9-]+$"},"description":{"type":"string"},"venueId":{"anyOf":[{"type":"string"},{"type":"null"}]},"dates":{"minItems":1,"type":"array","items":{"type":"object","properties":{"startsAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"endsAt":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"timezone":{"default":"America/Bogota","type":"string"}},"required":["startsAt","timezone"],"additionalProperties":false}}},"required":["name","slug","dates"],"additionalProperties":false},"EventUpdate":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"venueId":{"anyOf":[{"type":"string"},{"type":"null"}]},"coverImageUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}]}},"additionalProperties":false},"TicketType":{"type":"object","properties":{"id":{"type":"string"},"eventDateId":{"type":"string"},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"price":{"type":"number"},"currency":{"type":"string"},"capacity":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"maxPerOrder":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"saleStartsAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"saleEndsAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"isVisible":{"type":"boolean"},"organizerAbsorbsFee":{"type":"boolean"},"createdAt":{"type":"string"}},"required":["id","eventDateId","name","description","price","currency","capacity","maxPerOrder","saleStartsAt","saleEndsAt","isVisible","organizerAbsorbsFee","createdAt"],"additionalProperties":false},"TicketTypeCreate":{"type":"object","properties":{"eventDateId":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string"},"price":{"type":"number","minimum":0},"currency":{"default":"COP","type":"string"},"capacity":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxPerOrder":{"default":10,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"isVisible":{"default":true,"type":"boolean"},"organizerAbsorbsFee":{"default":false,"type":"boolean"}},"required":["eventDateId","name","price","currency","capacity","maxPerOrder","isVisible","organizerAbsorbsFee"],"additionalProperties":false},"SaleItem":{"type":"object","properties":{"id":{"type":"string"},"ticketTypeId":{"type":"string"},"quantity":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"unitPrice":{"type":"number"},"subtotal":{"type":"number"},"ticketCode":{"type":"string"},"checkedInAt":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","ticketTypeId","quantity","unitPrice","subtotal","ticketCode","checkedInAt"],"additionalProperties":false},"Sale":{"type":"object","properties":{"id":{"type":"string"},"reference":{"type":"string"},"status":{"type":"string","enum":["PENDING","CONFIRMED","ABANDONED","CANCELLED","REFUNDED"]},"channel":{"type":"string"},"subtotal":{"type":"number"},"discountAmount":{"type":"number"},"serviceFee":{"type":"number"},"total":{"type":"number"},"currency":{"type":"string"},"buyerName":{"type":"string"},"buyerEmail":{"type":"string"},"buyerPhone":{"anyOf":[{"type":"string"},{"type":"null"}]},"organizationId":{"anyOf":[{"type":"string"},{"type":"null"}]},"confirmedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["id","reference","status","channel","subtotal","discountAmount","serviceFee","total","currency","buyerName","buyerEmail","buyerPhone","organizationId","confirmedAt","createdAt"],"additionalProperties":false},"SaleDetail":{"type":"object","properties":{"id":{"type":"string"},"reference":{"type":"string"},"status":{"type":"string","enum":["PENDING","CONFIRMED","ABANDONED","CANCELLED","REFUNDED"]},"channel":{"type":"string"},"subtotal":{"type":"number"},"discountAmount":{"type":"number"},"serviceFee":{"type":"number"},"total":{"type":"number"},"currency":{"type":"string"},"buyerName":{"type":"string"},"buyerEmail":{"type":"string"},"buyerPhone":{"anyOf":[{"type":"string"},{"type":"null"}]},"organizationId":{"anyOf":[{"type":"string"},{"type":"null"}]},"confirmedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"},"items":{"type":"array","items":{"$ref":"#/components/schemas/SaleItem"}}},"required":["id","reference","status","channel","subtotal","discountAmount","serviceFee","total","currency","buyerName","buyerEmail","buyerPhone","organizationId","confirmedAt","createdAt","items"],"additionalProperties":false},"MembershipPlan":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"anyOf":[{"type":"string"},{"type":"null"}]},"name":{"type":"string"},"description":{"anyOf":[{"type":"string"},{"type":"null"}]},"price":{"type":"number"},"currency":{"type":"string"},"billingCycle":{"type":"string","enum":["MONTHLY","QUARTERLY","ANNUAL","LIFETIME"]},"benefits":{"type":"object","properties":{"presale":{"type":"boolean"},"freeTicket":{"type":"boolean"},"discount":{"type":"boolean"},"exclusiveContent":{"type":"boolean"},"merch":{"type":"boolean"}},"required":["presale","freeTicket","discount","exclusiveContent","merch"],"additionalProperties":false},"isActive":{"type":"boolean"},"sortOrder":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"createdAt":{"type":"string"}},"required":["id","organizationId","name","description","price","currency","billingCycle","benefits","isActive","sortOrder","createdAt"],"additionalProperties":false},"MembershipPlanCreate":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"price":{"type":"number","minimum":0},"currency":{"default":"COP","type":"string"},"billingCycle":{"type":"string","enum":["MONTHLY","QUARTERLY","ANNUAL","LIFETIME"]},"benefitPresale":{"default":false,"type":"boolean"},"benefitFreeTicket":{"default":false,"type":"boolean"},"benefitDiscount":{"default":false,"type":"boolean"},"benefitExclusiveContent":{"default":false,"type":"boolean"},"benefitMerch":{"default":false,"type":"boolean"},"isActive":{"default":true,"type":"boolean"}},"required":["name","price","currency","billingCycle","benefitPresale","benefitFreeTicket","benefitDiscount","benefitExclusiveContent","benefitMerch","isActive"],"additionalProperties":false},"Venue":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"country":{"type":"string"},"latitude":{"anyOf":[{"type":"number"},{"type":"null"}]},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}]},"capacity":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"googlePlaceId":{"anyOf":[{"type":"string"},{"type":"null"}]},"portalVisible":{"type":"boolean"},"organizationId":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["id","name","slug","address","city","country","latitude","longitude","capacity","googlePlaceId","portalVisible","organizationId","createdAt"],"additionalProperties":false},"VenueCreate":{"type":"object","properties":{"name":{"type":"string","minLength":1},"address":{"type":"string","minLength":1},"city":{"type":"string","minLength":1},"country":{"default":"CO","type":"string"},"capacity":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180}},"required":["name","address","city","country"],"additionalProperties":false},"StaffUser":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"role":{"type":"string","enum":["SUPER_ADMIN","ADMIN","STAFF","VIEWER"]},"organizationId":{"anyOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string"}},"required":["id","name","email","role","organizationId","createdAt"],"additionalProperties":false},"StaffCreate":{"type":"object","properties":{"name":{"type":"string","minLength":1},"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"}},"required":["name","email"],"additionalProperties":false},"RoleUpdate":{"type":"object","properties":{"role":{"type":"string","enum":["SUPER_ADMIN","ADMIN","STAFF","VIEWER"]}},"required":["role"],"additionalProperties":false},"ReportSummary":{"type":"object","properties":{"period":{"type":"string"},"currency":{"type":"string"},"revenue":{"type":"number"},"ticketsSold":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"salesCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"activeMemberships":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["period","currency","revenue","ticketsSold","salesCount","activeMemberships"],"additionalProperties":false}},"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"Authorization","description":"API key con prefijo `Bearer `. Alternativa: header `x-api-key` con la key cruda."}}}} \ No newline at end of file +{ + "openapi": "3.1.0", + "info": { + "title": "FreeTicket B2B API", + "version": "1.1.0", + "description": "API REST B2B de FreeTicket para integraciones y el CLI. Autenticación por API key (header `Authorization: Bearer ` o `x-api-key`); el workspace activo se selecciona con el header `X-Workspace-Id`." + }, + "servers": [ + { + "url": "https://admin.appfreeticket.com/api/v1" + } + ], + "security": [ + { + "apiKey": [] + } + ], + "paths": { + "/me": { + "get": { + "operationId": "getMe", + "tags": [ + "me" + ], + "summary": "Usuario autenticado, rol y workspaces accesibles", + "description": "Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Me" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/events": { + "get": { + "operationId": "getEvents", + "tags": [ + "events" + ], + "summary": "Listar eventos del workspace", + "description": "Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "description": "1-100, default 20", + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "description": "id del último resultado de la página previa", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Event" + } + }, + "page": { + "$ref": "#/components/schemas/Page" + } + }, + "required": [ + "data", + "page" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "operationId": "postEvents", + "tags": [ + "events" + ], + "summary": "Crear evento (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventCreate" + } + } + } + }, + "responses": { + "201": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Event" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/events/{id}": { + "get": { + "operationId": "getEventsId", + "tags": [ + "events" + ], + "summary": "Obtener un evento", + "description": "Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Event" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "operationId": "patchEventsId", + "tags": [ + "events" + ], + "summary": "Actualizar evento (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EventUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Event" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteEventsId", + "tags": [ + "events" + ], + "summary": "Eliminar (soft) evento (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/events/{id}/publish": { + "post": { + "operationId": "postEventsIdPublish", + "tags": [ + "events" + ], + "summary": "Publicar evento (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Event" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/events/{id}/dates": { + "get": { + "operationId": "getEventsIdDates", + "tags": [ + "events" + ], + "summary": "Listar fechas de un evento", + "description": "Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EventDate" + } + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "operationId": "postEventsIdDates", + "tags": [ + "events" + ], + "summary": "Agregar fecha a un evento (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/EventDate" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/events/{id}/dates/{dateId}": { + "patch": { + "operationId": "patchEventsIdDatesDateId", + "tags": [ + "events" + ], + "summary": "Actualizar fecha (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "dateId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/EventDate" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteEventsIdDatesDateId", + "tags": [ + "events" + ], + "summary": "Eliminar fecha (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "dateId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/ticket-types": { + "get": { + "operationId": "getTicketTypes", + "tags": [ + "ticket-types" + ], + "summary": "Listar tipos de ticket del workspace", + "description": "Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "eventDateId", + "in": "query", + "required": false, + "description": "filtra por fecha de evento", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TicketType" + } + }, + "page": { + "$ref": "#/components/schemas/Page" + } + }, + "required": [ + "data", + "page" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "operationId": "postTicketTypes", + "tags": [ + "ticket-types" + ], + "summary": "Crear tipo de ticket (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TicketTypeCreate" + } + } + } + }, + "responses": { + "201": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/TicketType" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/ticket-types/{id}": { + "get": { + "operationId": "getTicketTypesId", + "tags": [ + "ticket-types" + ], + "summary": "Obtener un tipo de ticket", + "description": "Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/TicketType" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "operationId": "patchTicketTypesId", + "tags": [ + "ticket-types" + ], + "summary": "Actualizar tipo de ticket (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/TicketType" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteTicketTypesId", + "tags": [ + "ticket-types" + ], + "summary": "Eliminar (soft) tipo de ticket (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/sales": { + "get": { + "operationId": "getSales", + "tags": [ + "sales" + ], + "summary": "Listar ventas del workspace", + "description": "Rol mínimo: `STAFF`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "status", + "in": "query", + "required": false, + "description": "PENDING|CONFIRMED|ABANDONED|CANCELLED|REFUNDED", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Sale" + } + }, + "page": { + "$ref": "#/components/schemas/Page" + } + }, + "required": [ + "data", + "page" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/sales/{id}": { + "get": { + "operationId": "getSalesId", + "tags": [ + "sales" + ], + "summary": "Detalle de una venta con sus ítems", + "description": "Rol mínimo: `STAFF`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/SaleDetail" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/sales/{id}/cancel": { + "post": { + "operationId": "postSalesIdCancel", + "tags": [ + "sales" + ], + "summary": "Cancelar una venta (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Sale" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/sales/{id}/refund": { + "post": { + "operationId": "postSalesIdRefund", + "tags": [ + "sales" + ], + "summary": "Reembolsar una venta (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Sale" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/membership-plans": { + "get": { + "operationId": "getMembershipPlans", + "tags": [ + "membership-plans" + ], + "summary": "Listar planes de membresía del workspace", + "description": "Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MembershipPlan" + } + }, + "page": { + "$ref": "#/components/schemas/Page" + } + }, + "required": [ + "data", + "page" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "operationId": "postMembershipPlans", + "tags": [ + "membership-plans" + ], + "summary": "Crear plan de membresía (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MembershipPlanCreate" + } + } + } + }, + "responses": { + "201": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/MembershipPlan" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/membership-plans/{id}": { + "get": { + "operationId": "getMembershipPlansId", + "tags": [ + "membership-plans" + ], + "summary": "Obtener un plan de membresía", + "description": "Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/MembershipPlan" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "operationId": "patchMembershipPlansId", + "tags": [ + "membership-plans" + ], + "summary": "Actualizar plan (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/MembershipPlan" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteMembershipPlansId", + "tags": [ + "membership-plans" + ], + "summary": "Eliminar (soft) plan (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/venues": { + "get": { + "operationId": "getVenues", + "tags": [ + "venues" + ], + "summary": "Listar venues del workspace", + "description": "Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Venue" + } + }, + "page": { + "$ref": "#/components/schemas/Page" + } + }, + "required": [ + "data", + "page" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "operationId": "postVenues", + "tags": [ + "venues" + ], + "summary": "Crear venue (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VenueCreate" + } + } + } + }, + "responses": { + "201": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Venue" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/venues/{id}": { + "get": { + "operationId": "getVenuesId", + "tags": [ + "venues" + ], + "summary": "Obtener un venue", + "description": "Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Venue" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "patch": { + "operationId": "patchVenuesId", + "tags": [ + "venues" + ], + "summary": "Actualizar venue (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/Venue" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "operationId": "deleteVenuesId", + "tags": [ + "venues" + ], + "summary": "Eliminar (soft) venue (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/staff": { + "get": { + "operationId": "getStaff", + "tags": [ + "staff" + ], + "summary": "Listar staff con acceso al workspace", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StaffUser" + } + }, + "page": { + "$ref": "#/components/schemas/Page" + } + }, + "required": [ + "data", + "page" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "operationId": "postStaff", + "tags": [ + "staff" + ], + "summary": "Crear usuario staff (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StaffCreate" + } + } + } + }, + "responses": { + "201": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/StaffUser" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/staff/{id}/role": { + "patch": { + "operationId": "patchStaffIdRole", + "tags": [ + "staff" + ], + "summary": "Cambiar rol de un usuario staff (fase 2)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/StaffUser" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/reports/summary": { + "get": { + "operationId": "getReportsSummary", + "tags": [ + "reports" + ], + "summary": "KPIs del workspace (revenue, tickets, ventas, membresías)", + "description": "Rol mínimo: `VIEWER`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "period", + "in": "query", + "required": false, + "description": "7d | 30d (default) | 90d | 1y", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/ReportSummary" + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/reports/reconciliation": { + "get": { + "operationId": "getReportsReconciliation", + "tags": [ + "reports" + ], + "summary": "Conciliación financiera CFO (MP ↔ venta ↔ factura Siigo)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "date_from", + "in": "query", + "required": true, + "description": "ISO 8601 (inicio, sobre Sale.created_at)", + "schema": { + "type": "string" + } + }, + { + "name": "date_to", + "in": "query", + "required": true, + "description": "ISO 8601 (fin)", + "schema": { + "type": "string" + } + }, + { + "name": "match_status", + "in": "query", + "required": false, + "description": "Filtra por flag: OK|MISSING_INVOICE|MISSING_CUFE|AMOUNT_MISMATCH|MISSING_PAYMENT", + "schema": { + "type": "string" + } + }, + { + "name": "provider", + "in": "query", + "required": false, + "description": "Filtra por payment_provider (ej: mercadopago)", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "description": "Página 1-based (default 1)", + "schema": { + "type": "string" + } + }, + { + "name": "page_size", + "in": "query", + "required": false, + "description": "Tamaño de página 1..500 (default 100)", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReconciliationRow" + } + } + }, + "required": [ + "data" + ] + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/reports/exports/reconciliation": { + "get": { + "operationId": "getReportsExportsReconciliation", + "tags": [ + "reports" + ], + "summary": "Export CSV de la conciliación financiera CFO", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "parameters": [ + { + "name": "date_from", + "in": "query", + "required": true, + "description": "ISO 8601 (inicio, sobre Sale.created_at)", + "schema": { + "type": "string" + } + }, + { + "name": "date_to", + "in": "query", + "required": true, + "description": "ISO 8601 (fin)", + "schema": { + "type": "string" + } + }, + { + "name": "match_status", + "in": "query", + "required": false, + "description": "Filtra por flag: OK|MISSING_INVOICE|MISSING_CUFE|AMOUNT_MISMATCH|MISSING_PAYMENT", + "schema": { + "type": "string" + } + }, + { + "name": "provider", + "in": "query", + "required": false, + "description": "Filtra por payment_provider (ej: mercadopago)", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/reports/exports/buyers": { + "get": { + "operationId": "getReportsExportsBuyers", + "tags": [ + "reports" + ], + "summary": "Export de compradores (máx 5000)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/reports/exports/subscribers": { + "get": { + "operationId": "getReportsExportsSubscribers", + "tags": [ + "reports" + ], + "summary": "Export de suscriptores (máx 5000)", + "description": "Rol mínimo: `ADMIN`. Workspace activo vía header `X-Workspace-Id`.", + "security": [ + { + "apiKey": [] + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + }, + "401": { + "description": "Credencial inválida o ausente.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Rol insuficiente o recurso no accesible.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Recurso inexistente o fuera de alcance.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "422": { + "description": "Validación del cuerpo/parámetros.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Error": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "details": {} + }, + "required": [ + "code", + "message" + ], + "additionalProperties": false + } + }, + "required": [ + "error" + ], + "additionalProperties": false + }, + "Page": { + "type": "object", + "properties": { + "nextCursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean" + } + }, + "required": [ + "nextCursor", + "hasMore" + ], + "additionalProperties": false + }, + "Workspace": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "slug" + ], + "additionalProperties": false + }, + "Me": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "role": { + "type": "string", + "enum": [ + "SUPER_ADMIN", + "ADMIN", + "STAFF", + "VIEWER", + "MINCULTURA" + ] + }, + "activeWorkspaceId": { + "type": "string" + }, + "workspaces": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Workspace" + } + } + }, + "required": [ + "userId", + "name", + "email", + "role", + "activeWorkspaceId", + "workspaces" + ], + "additionalProperties": false + }, + "Event": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "DRAFT", + "PUBLISHED", + "SOLD_OUT", + "CANCELLED", + "COMPLETED" + ] + }, + "coverImageUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "organizationId": { + "type": "string" + }, + "venueId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "status", + "coverImageUrl", + "organizationId", + "venueId", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + }, + "EventDate": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "eventId": { + "type": "string" + }, + "label": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "startsAt": { + "type": "string" + }, + "endsAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "doorsOpenAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "timezone": { + "type": "string" + }, + "venueId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string", + "enum": [ + "DRAFT", + "PUBLISHED", + "SOLD_OUT", + "CANCELLED", + "COMPLETED" + ] + }, + "streamType": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "required": [ + "id", + "eventId", + "label", + "startsAt", + "endsAt", + "doorsOpenAt", + "timezone", + "venueId", + "status", + "streamType", + "createdAt" + ], + "additionalProperties": false + }, + "EventCreate": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z0-9-]+$" + }, + "description": { + "type": "string" + }, + "venueId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "dates": { + "minItems": 1, + "type": "array", + "items": { + "type": "object", + "properties": { + "startsAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "endsAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "timezone": { + "default": "America/Bogota", + "type": "string" + } + }, + "required": [ + "startsAt", + "timezone" + ], + "additionalProperties": false + } + } + }, + "required": [ + "name", + "slug", + "dates" + ], + "additionalProperties": false + }, + "EventUpdate": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "venueId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "coverImageUrl": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "TicketType": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "eventDateId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "price": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "capacity": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "maxPerOrder": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "saleStartsAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "saleEndsAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "isVisible": { + "type": "boolean" + }, + "organizerAbsorbsFee": { + "type": "boolean" + }, + "createdAt": { + "type": "string" + } + }, + "required": [ + "id", + "eventDateId", + "name", + "description", + "price", + "currency", + "capacity", + "maxPerOrder", + "saleStartsAt", + "saleEndsAt", + "isVisible", + "organizerAbsorbsFee", + "createdAt" + ], + "additionalProperties": false + }, + "TicketTypeCreate": { + "type": "object", + "properties": { + "eventDateId": { + "type": "string", + "minLength": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "price": { + "type": "number", + "minimum": 0 + }, + "currency": { + "default": "COP", + "type": "string" + }, + "capacity": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxPerOrder": { + "default": 10, + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "isVisible": { + "default": true, + "type": "boolean" + }, + "organizerAbsorbsFee": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "eventDateId", + "name", + "price", + "currency", + "capacity", + "maxPerOrder", + "isVisible", + "organizerAbsorbsFee" + ], + "additionalProperties": false + }, + "SaleItem": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "ticketTypeId": { + "type": "string" + }, + "quantity": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "unitPrice": { + "type": "number" + }, + "subtotal": { + "type": "number" + }, + "ticketCode": { + "type": "string" + }, + "checkedInAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "ticketTypeId", + "quantity", + "unitPrice", + "subtotal", + "ticketCode", + "checkedInAt" + ], + "additionalProperties": false + }, + "Sale": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "PENDING", + "CONFIRMED", + "ABANDONED", + "CANCELLED", + "REFUNDED" + ] + }, + "channel": { + "type": "string" + }, + "subtotal": { + "type": "number" + }, + "discountAmount": { + "type": "number" + }, + "serviceFee": { + "type": "number" + }, + "iva": { + "type": "number" + }, + "total": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "buyerName": { + "type": "string" + }, + "buyerEmail": { + "type": "string" + }, + "buyerPhone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "organizationId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "confirmedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string" + } + }, + "required": [ + "id", + "reference", + "status", + "channel", + "subtotal", + "discountAmount", + "serviceFee", + "iva", + "total", + "currency", + "buyerName", + "buyerEmail", + "buyerPhone", + "organizationId", + "confirmedAt", + "createdAt" + ], + "additionalProperties": false + }, + "SaleDetail": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "reference": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "PENDING", + "CONFIRMED", + "ABANDONED", + "CANCELLED", + "REFUNDED" + ] + }, + "channel": { + "type": "string" + }, + "subtotal": { + "type": "number" + }, + "discountAmount": { + "type": "number" + }, + "serviceFee": { + "type": "number" + }, + "iva": { + "type": "number" + }, + "total": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "buyerName": { + "type": "string" + }, + "buyerEmail": { + "type": "string" + }, + "buyerPhone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "organizationId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "confirmedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SaleItem" + } + } + }, + "required": [ + "id", + "reference", + "status", + "channel", + "subtotal", + "discountAmount", + "serviceFee", + "iva", + "total", + "currency", + "buyerName", + "buyerEmail", + "buyerPhone", + "organizationId", + "confirmedAt", + "createdAt", + "items" + ], + "additionalProperties": false + }, + "MembershipPlan": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "organizationId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "price": { + "type": "number" + }, + "currency": { + "type": "string" + }, + "billingCycle": { + "type": "string", + "enum": [ + "MONTHLY", + "QUARTERLY", + "ANNUAL", + "LIFETIME" + ] + }, + "benefits": { + "type": "object", + "properties": { + "presale": { + "type": "boolean" + }, + "freeTicket": { + "type": "boolean" + }, + "discount": { + "type": "boolean" + }, + "exclusiveContent": { + "type": "boolean" + }, + "merch": { + "type": "boolean" + } + }, + "required": [ + "presale", + "freeTicket", + "discount", + "exclusiveContent", + "merch" + ], + "additionalProperties": false + }, + "isActive": { + "type": "boolean" + }, + "sortOrder": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "createdAt": { + "type": "string" + } + }, + "required": [ + "id", + "organizationId", + "name", + "description", + "price", + "currency", + "billingCycle", + "benefits", + "isActive", + "sortOrder", + "createdAt" + ], + "additionalProperties": false + }, + "MembershipPlanCreate": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "price": { + "type": "number", + "minimum": 0 + }, + "currency": { + "default": "COP", + "type": "string" + }, + "billingCycle": { + "type": "string", + "enum": [ + "MONTHLY", + "QUARTERLY", + "ANNUAL", + "LIFETIME" + ] + }, + "benefitPresale": { + "default": false, + "type": "boolean" + }, + "benefitFreeTicket": { + "default": false, + "type": "boolean" + }, + "benefitDiscount": { + "default": false, + "type": "boolean" + }, + "benefitExclusiveContent": { + "default": false, + "type": "boolean" + }, + "benefitMerch": { + "default": false, + "type": "boolean" + }, + "isActive": { + "default": true, + "type": "boolean" + } + }, + "required": [ + "name", + "price", + "currency", + "billingCycle", + "benefitPresale", + "benefitFreeTicket", + "benefitDiscount", + "benefitExclusiveContent", + "benefitMerch", + "isActive" + ], + "additionalProperties": false + }, + "Venue": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "address": { + "type": "string" + }, + "city": { + "type": "string" + }, + "country": { + "type": "string" + }, + "latitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "longitude": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "capacity": { + "anyOf": [ + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + { + "type": "null" + } + ] + }, + "googlePlaceId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "portalVisible": { + "type": "boolean" + }, + "organizationId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "slug", + "address", + "city", + "country", + "latitude", + "longitude", + "capacity", + "googlePlaceId", + "portalVisible", + "organizationId", + "createdAt" + ], + "additionalProperties": false + }, + "VenueCreate": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "address": { + "type": "string", + "minLength": 1 + }, + "city": { + "type": "string", + "minLength": 1 + }, + "country": { + "default": "CO", + "type": "string" + }, + "capacity": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "latitude": { + "type": "number", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "minimum": -180, + "maximum": 180 + } + }, + "required": [ + "name", + "address", + "city", + "country" + ], + "additionalProperties": false + }, + "StaffUser": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "role": { + "type": "string", + "enum": [ + "SUPER_ADMIN", + "ADMIN", + "STAFF", + "VIEWER", + "MINCULTURA" + ] + }, + "organizationId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "email", + "role", + "organizationId", + "createdAt" + ], + "additionalProperties": false + }, + "StaffCreate": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "email": { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + } + }, + "required": [ + "name", + "email" + ], + "additionalProperties": false + }, + "RoleUpdate": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": [ + "SUPER_ADMIN", + "ADMIN", + "STAFF", + "VIEWER", + "MINCULTURA" + ] + } + }, + "required": [ + "role" + ], + "additionalProperties": false + }, + "ReportSummary": { + "type": "object", + "properties": { + "period": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "revenue": { + "type": "number" + }, + "ticketsSold": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "salesCount": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "activeMemberships": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + }, + "required": [ + "period", + "currency", + "revenue", + "ticketsSold", + "salesCount", + "activeMemberships" + ], + "additionalProperties": false + }, + "MatchStatus": { + "type": "string", + "enum": [ + "OK", + "MISSING_INVOICE", + "MISSING_CUFE", + "AMOUNT_MISMATCH", + "MISSING_PAYMENT" + ] + }, + "ReconciliationRow": { + "type": "object", + "properties": { + "sale_reference": { + "type": "string" + }, + "sale_status": { + "type": "string", + "enum": [ + "PENDING", + "CONFIRMED", + "ABANDONED", + "CANCELLED", + "REFUNDED" + ] + }, + "sale_amount": { + "type": "number" + }, + "mp_payment_ref": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "mp_provider": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "mp_amount": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "siigo_invoice_name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "siigo_invoice_status": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "siigo_cufe": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "match_status": { + "$ref": "#/components/schemas/MatchStatus" + } + }, + "required": [ + "sale_reference", + "sale_status", + "sale_amount", + "mp_payment_ref", + "mp_provider", + "mp_amount", + "siigo_invoice_name", + "siigo_invoice_status", + "siigo_cufe", + "match_status" + ], + "additionalProperties": false + } + }, + "securitySchemes": { + "apiKey": { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": "API key con prefijo `Bearer `. Alternativa: header `x-api-key` con la key cruda." + } + } + } +} diff --git a/package.json b/package.json index 922d0d2..c34b26a 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,14 @@ { "name": "@freeticket/cli", - "version": "0.1.1", + "version": "0.4.0", "description": "Official FreeTicket CLI — consumes the B2B REST API for events, sales, tickets, memberships, venues, staff, and reports.", - "keywords": ["freeticket", "cli", "ticketing", "events", "api"], + "keywords": [ + "freeticket", + "cli", + "ticketing", + "events", + "api" + ], "homepage": "https://github.com/AppFreeticket/freeticket-cli#readme", "bugs": "https://github.com/AppFreeticket/freeticket-cli/issues", "repository": { @@ -16,7 +22,9 @@ "bin": { "ft": "dist/index.js" }, - "files": ["dist"], + "files": [ + "dist" + ], "engines": { "node": ">=20" }, @@ -26,8 +34,9 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", - "generate": "openapi-ts", + "generate": "openapi-ts && openapi-ts -f openapi-ts.admin.config.ts", "sync-openapi": "tsx scripts/sync-openapi.ts", + "sync-openapi:admin": "FT_OPENAPI_URL=${FT_ADMIN_OPENAPI_URL:-https://admin.appfreeticket.com/api/admin/openapi.json} FT_OPENAPI_OUT=admin-openapi.json tsx scripts/sync-openapi.ts", "lint": "biome check .", "format": "biome check --write .", "test": "vitest run", diff --git a/scripts/sync-openapi.ts b/scripts/sync-openapi.ts index 3361154..8120f1c 100644 --- a/scripts/sync-openapi.ts +++ b/scripts/sync-openapi.ts @@ -12,9 +12,10 @@ if (!res.ok) { console.error(`Could not download the spec (${res.status}) from ${url}`); process.exit(1); } +const out = process.env.FT_OPENAPI_OUT ?? "openapi.json"; const spec = await res.json(); -writeFileSync("openapi.json", `${JSON.stringify(spec, null, 2)}\n`); -console.log(`✓ openapi.json updated from ${url}`); +writeFileSync(out, `${JSON.stringify(spec, null, 2)}\n`); +console.log(`✓ ${out} updated from ${url}`); execSync("pnpm generate", { stdio: "inherit" }); -console.log("✓ client regenerated in src/client/"); +console.log("✓ clients regenerated in src/client/ + src/admin-client/"); diff --git a/src/commands/admin.ts b/src/commands/admin.ts new file mode 100644 index 0000000..3d197f7 --- /dev/null +++ b/src/commands/admin.ts @@ -0,0 +1,299 @@ +// biome-ignore-all lint/suspicious/noExplicitAny: generated SDK boundary — signatures vary by resource. +import type { Command } from "commander"; +import { + getAuditLog, + getFeatureFlags, + getMe, + getPlatformPlans, + getPlatformPlansId, + getUsers, + getUsersId, + getWorkspaces, + getWorkspacesId, + patchPlatformPlansId, + patchUsersId, + patchWorkspacesId, + postImpersonate, + postImpersonateStop, + postPlatformPlans, + postWorkspaces, + postWorkspacesIdRestore, + postWorkspacesIdSuspend, + putFeatureFlagsKey, +} from "../admin-client/sdk.gen"; +import { configureAdminClient, unwrap } from "../lib/api"; +import { confirm, parseData } from "../lib/input"; +import { print, printNextCursor, toCsv } from "../lib/output"; + +type SdkFn = ( + opts: any, +) => Promise<{ data?: any; error?: unknown; response: Response }>; + +/** Id-bound (or key-bound) custom mutation, e.g. workspaces suspend. */ +interface AdminAction { + name: string; + describe: string; + fn: SdkFn; + /** Accepts a --data JSON body. */ + body?: boolean; + /** Path param name (default "id"; feature-flags use "key"). */ + param?: string; + /** Confirm before running (destructive). */ + confirm?: boolean; +} + +interface AdminResource { + name: string; + describe: string; + list?: SdkFn; + get?: SdkFn; + create?: SdkFn; + update?: SdkFn; + actions?: AdminAction[]; + columns?: string[]; + /** Extra list flags -> query entries. */ + listFlags?: { flag: string; describe: string; query: string }[]; + /** This list endpoint has no cursor pagination (e.g. feature-flags). */ + noPaging?: boolean; +} + +/** + * Superadmin commands (`ft admin `) against the /api/admin contract. + * Reads + writes (create/update, suspend/restore, feature-flag set, + * impersonate) behind confirmation on destructive ops. + * Auth is a SUPER_ADMIN session, not an API key (see configureAdminClient). + */ +export function registerAdmin(program: Command): void { + const admin = program + .command("admin") + .description("Superadmin (cross-tenant) — requires FT_ADMIN_SESSION"); + + admin + .command("me") + .description("Current superadmin identity (GET /api/admin/me)") + .option("--json", "raw JSON output") + .action(async (opts) => { + configureAdminClient(); + const body = unwrap(await getMe({})); + print(body.data, { json: opts.json }); + }); + + admin + .command("impersonate") + .description('Start impersonation (--data \'{"targetUserId":"..."}\')') + .requiredOption("--data ", "JSON body (inline or @file.json)") + .option("--json", "raw JSON output") + .action(async (opts) => { + configureAdminClient(); + const body = unwrap( + await postImpersonate({ body: parseData(opts.data) as never }), + ); + print(body?.data ?? body, { json: opts.json }); + }); + + admin + .command("impersonate-stop") + .description("Stop the current impersonation") + .option("--json", "raw JSON output") + .action(async (opts) => { + configureAdminClient(); + const body = unwrap(await postImpersonateStop({})); + print(body?.data ?? { ok: true }, { json: opts.json }); + }); + + const resources: AdminResource[] = [ + { + name: "workspaces", + describe: "Tenants / workspaces", + list: getWorkspaces, + get: getWorkspacesId, + create: postWorkspaces, + update: patchWorkspacesId, + actions: [ + { + name: "suspend", + describe: "Suspend a workspace", + fn: postWorkspacesIdSuspend, + confirm: true, + }, + { + name: "restore", + describe: "Restore a suspended workspace", + fn: postWorkspacesIdRestore, + }, + ], + columns: ["id", "name", "type", "country", "suspended", "createdAt"], + listFlags: [ + { flag: "--status ", describe: "filter by status", query: "status" }, + { flag: "--q ", describe: "search by name/slug", query: "q" }, + ], + }, + { + name: "users", + describe: "Global users (cross-tenant)", + list: getUsers, + get: getUsersId, + update: patchUsersId, + columns: ["id", "name", "email", "role", "banned", "createdAt"], + listFlags: [ + { flag: "--q ", describe: "search by name/email", query: "q" }, + { flag: "--role ", describe: "filter by role", query: "role" }, + { + flag: "--workspace ", + describe: "filter by workspace", + query: "workspaceId", + }, + ], + }, + { + name: "plans", + describe: "Platform plans", + list: getPlatformPlans, + get: getPlatformPlansId, + create: postPlatformPlans, + update: patchPlatformPlansId, + columns: ["id", "name", "price", "currency", "interval"], + }, + { + name: "feature-flags", + describe: "Feature flags", + list: getFeatureFlags, + actions: [ + { + name: "set", + describe: 'Set a flag (--data \'{"scope":"...","enabled":true}\')', + fn: putFeatureFlagsKey, + body: true, + param: "key", + }, + ], + columns: ["key", "enabled", "description"], + noPaging: true, + }, + { + name: "audit-log", + describe: "Superadmin audit log", + list: getAuditLog, + columns: ["id", "actorId", "action", "targetType", "createdAt"], + listFlags: [ + { flag: "--actor ", describe: "filter by actor", query: "actorId" }, + { flag: "--action ", describe: "filter by action", query: "action" }, + { flag: "--from ", describe: "from (ISO 8601)", query: "from" }, + { flag: "--to ", describe: "to (ISO 8601)", query: "to" }, + ], + }, + ]; + + for (const spec of resources) registerAdminResource(admin, spec); +} + +function registerAdminResource(parent: Command, spec: AdminResource): void { + const root = parent.command(spec.name).description(spec.describe); + const singular = spec.name.replace(/s$/, ""); + + if (spec.list) { + const list = spec.list; + const cmd = root.command("list").description(`List ${spec.name}`); + if (!spec.noPaging) { + cmd + .option("--limit ", "results per page (1-100)", "20") + .option("--cursor ", "pagination cursor"); + } + for (const f of spec.listFlags ?? []) cmd.option(f.flag, f.describe); + cmd.option("--csv", "CSV output"); + cmd.option("--json", "raw JSON output"); + cmd.action(async (opts) => { + configureAdminClient(); + const query: Record = {}; + if (!spec.noPaging) { + query.limit = Number(opts.limit); + query.cursor = opts.cursor; + } + for (const f of spec.listFlags ?? []) { + const v = opts[camel(f.query)]; + if (v !== undefined) query[f.query] = v; + } + const body = unwrap(await list({ query })); + if (opts.csv) { + process.stdout.write(`${toCsv(body.data, spec.columns)}\n`); + return; + } + print(body.data, { json: opts.json, columns: spec.columns }); + if (!opts.json && !spec.noPaging) printNextCursor(body.page); + }); + } + + if (spec.get) { + const get = spec.get; + root + .command("get ") + .description(`Get one ${singular} by id`) + .option("--json", "raw JSON output") + .action(async (id, opts) => { + configureAdminClient(); + const body = unwrap(await get({ path: { id } })); + print(body.data, { json: opts.json }); + }); + } + + if (spec.create) { + const create = spec.create; + root + .command("create") + .description(`Create a ${singular}`) + .requiredOption("--data ", "JSON body (inline or @file.json)") + .option("--json", "raw JSON output") + .action(async (opts) => { + configureAdminClient(); + const body = unwrap(await create({ body: parseData(opts.data) })); + print(body?.data ?? body, { json: opts.json }); + }); + } + + if (spec.update) { + const update = spec.update; + root + .command("update ") + .description(`Update a ${singular} by id`) + .requiredOption("--data ", "JSON body (inline or @file.json)") + .option("--json", "raw JSON output") + .action(async (id, opts) => { + configureAdminClient(); + const body = unwrap( + await update({ path: { id }, body: parseData(opts.data) }), + ); + print(body?.data ?? body, { json: opts.json }); + }); + } + + for (const action of spec.actions ?? []) { + const param = action.param ?? "id"; + const cmd = root + .command(`${action.name} <${param}>`) + .description(action.describe) + .option("--yes", "skip confirmation") + .option("--json", "raw JSON output"); + if (action.body) { + cmd.option("--data ", "JSON body (inline or @file.json)"); + } + cmd.action(async (value, opts) => { + if ( + action.confirm && + !opts.yes && + !(await confirm(`${action.name} ${singular} ${value}?`)) + ) { + console.error("Aborted."); + return; + } + configureAdminClient(); + const payload: Record = { path: { [param]: value } }; + if (action.body && opts.data) payload.body = parseData(opts.data); + const body = unwrap(await action.fn(payload)); + print(body?.data ?? body ?? { ok: true }, { json: opts.json }); + }); + } +} + +function camel(s: string): string { + return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); +} diff --git a/src/commands/event-dates.ts b/src/commands/event-dates.ts new file mode 100644 index 0000000..2e4832d --- /dev/null +++ b/src/commands/event-dates.ts @@ -0,0 +1,97 @@ +// biome-ignore-all lint/suspicious/noExplicitAny: generated SDK boundary — signatures vary by resource. +import type { Command } from "commander"; +import { configureClient, unwrap } from "../lib/api"; +import { confirm, parseData } from "../lib/input"; +import { print } from "../lib/output"; + +type SdkFn = ( + opts: any, +) => Promise<{ data?: any; error?: unknown; response: Response }>; + +interface EventDatesSpec { + /** GET /events/{id}/dates */ + list: SdkFn; + /** POST /events/{id}/dates */ + create: SdkFn; + /** PATCH /events/{id}/dates/{dateId} */ + update: SdkFn; + /** DELETE /events/{id}/dates/{dateId} */ + del: SdkFn; +} + +/** + * `ft event-dates ...` — the nested /events/{id}/dates resource. Lives apart + * from registerResource because its paths take two ids (event + date). + */ +export function registerEventDates( + program: Command, + spec: EventDatesSpec, +): void { + const root = program + .command("event-dates") + .description("Event dates (sessions of an event)"); + + root + .command("list ") + .description("List dates of an event") + .option("--workspace ", "workspace override") + .option("--json", "raw JSON output") + .action(async (eventId, opts) => { + configureClient(opts.workspace); + const body = unwrap(await spec.list({ path: { id: eventId } })); + print(body.data ?? body, { + json: opts.json, + columns: ["id", "startsAt", "endsAt", "status"], + }); + }); + + root + .command("create ") + .description("Add a date to an event") + .requiredOption("--data ", "JSON body (inline or @file.json)") + .option("--workspace ", "workspace override") + .option("--json", "raw JSON output") + .action(async (eventId, opts) => { + configureClient(opts.workspace); + const body = unwrap( + await spec.create({ + path: { id: eventId }, + body: parseData(opts.data), + }), + ); + print(body.data ?? body, { json: opts.json }); + }); + + root + .command("update ") + .description("Update an event date") + .requiredOption("--data ", "JSON body (inline or @file.json)") + .option("--workspace ", "workspace override") + .option("--json", "raw JSON output") + .action(async (eventId, dateId, opts) => { + configureClient(opts.workspace); + const body = unwrap( + await spec.update({ + path: { id: eventId, dateId }, + body: parseData(opts.data), + }), + ); + print(body.data ?? body, { json: opts.json }); + }); + + root + .command("delete ") + .description("Delete an event date") + .option("--yes", "skip confirmation") + .option("--workspace ", "workspace override") + .option("--json", "raw JSON output") + .action(async (eventId, dateId, opts) => { + if (!opts.yes && !(await confirm(`Delete date ${dateId}?`))) { + console.error("Aborted."); + return; + } + configureClient(opts.workspace); + const body = unwrap(await spec.del({ path: { id: eventId, dateId } })); + print(body?.data ?? { deleted: dateId }, { json: opts.json }); + }); +} diff --git a/src/commands/reports.ts b/src/commands/reports.ts index fa0506f..9be49c5 100644 --- a/src/commands/reports.ts +++ b/src/commands/reports.ts @@ -1,7 +1,9 @@ import type { Command } from "commander"; import { getReportsExportsBuyers, + getReportsExportsReconciliation, getReportsExportsSubscribers, + getReportsReconciliation, getReportsSummary, } from "../client/sdk.gen"; import { configureClient, unwrap } from "../lib/api"; @@ -24,9 +26,64 @@ export function registerReports(program: Command): void { print(body.data, { json: opts.json }); }); + root + .command("reconciliation") + .description("CFO reconciliation: Mercado Pago vs sale vs Siigo invoice") + .requiredOption("--from ", "date_from (ISO 8601)") + .requiredOption("--to ", "date_to (ISO 8601)") + .option( + "--match ", + "OK | MISSING_INVOICE | MISSING_CUFE | AMOUNT_MISMATCH | MISSING_PAYMENT", + ) + .option("--provider

", "filter by payment provider") + .option("--page ", "page number") + .option("--page-size ", "rows per page") + .option("--workspace ", "workspace override") + .option("--json", "raw JSON output") + .action(async (opts) => { + configureClient(opts.workspace); + const body = unwrap( + await getReportsReconciliation({ + query: { + date_from: opts.from, + date_to: opts.to, + match_status: opts.match, + provider: opts.provider, + page: opts.page, + page_size: opts.pageSize, + }, + }), + ); + print(body.data, { json: opts.json }); + }); + const exp = root .command("export") - .description("Export buyers or subscribers (CSV)"); + .description("Export buyers, subscribers or reconciliation (CSV)"); + + exp + .command("reconciliation") + .description("Export reconciliation (CSV) for accounting") + .requiredOption("--from ", "date_from (ISO 8601)") + .requiredOption("--to ", "date_to (ISO 8601)") + .option("--match ", "filter by match_status") + .option("--provider

", "filter by payment provider") + .option("--workspace ", "workspace override") + .option("--json", "raw JSON output") + .action(async (opts) => { + configureClient(opts.workspace); + const body = unwrap( + await getReportsExportsReconciliation({ + query: { + date_from: opts.from, + date_to: opts.to, + match_status: opts.match, + provider: opts.provider, + }, + }), + ); + print(body.data ?? body, { json: opts.json }); + }); for (const [name, fn, label] of [ ["buyers", getReportsExportsBuyers, "buyers"], diff --git a/src/commands/resource.ts b/src/commands/resource.ts index 46acb80..165d246 100644 --- a/src/commands/resource.ts +++ b/src/commands/resource.ts @@ -1,12 +1,23 @@ // biome-ignore-all lint/suspicious/noExplicitAny: generated SDK boundary — signatures vary by resource. import type { Command } from "commander"; import { configureClient, unwrap } from "../lib/api"; -import { print, printNextCursor } from "../lib/output"; +import { confirm, parseData } from "../lib/input"; +import { print, printNextCursor, toCsv } from "../lib/output"; type SdkFn = ( opts: any, ) => Promise<{ data?: any; error?: unknown; response: Response }>; +/** A custom mutation bound to one resource id, e.g. `events publish `. */ +interface ActionSpec { + name: string; + describe: string; + /** SDK function: receives { path: { id }, body? }. */ + fn: SdkFn; + /** When true, the action accepts a `--data` JSON body (e.g. refund amount). */ + body?: boolean; +} + interface ResourceSpec { name: string; describe: string; @@ -14,18 +25,29 @@ interface ResourceSpec { list?: SdkFn; /** SDK function for detail by id (path: { id }). */ get?: SdkFn; - /** Columns to show in list table output. */ + /** SDK function for create (body). */ + create?: SdkFn; + /** SDK function for update by id (path: { id }, body). */ + update?: SdkFn; + /** SDK function for delete by id (path: { id }). */ + del?: SdkFn; + /** Custom id-bound actions (publish, cancel, refund, ...). */ + actions?: ActionSpec[]; + /** Columns to show in list table / CSV output. */ columns?: string[]; /** Extra list flags -> query entries. Example: status, eventDateId. */ listFlags?: { flag: string; describe: string; query: string }[]; } /** - * Registers a ` list|get` subcommand wired to the generated SDK. - * All commands share --json and --workspace; list commands also get --limit/--cursor. + * Registers a ` list|get|create|update|delete|` command group + * wired to the generated SDK. All commands share --json and --workspace; + * list also gets --limit/--cursor/--csv; mutations take --data (inline JSON + * or @file); delete asks for confirmation unless --yes. */ export function registerResource(program: Command, spec: ResourceSpec): void { const root = program.command(spec.name).description(spec.describe); + const singular = spec.name.replace(/s$/, ""); if (spec.list) { const list = spec.list; @@ -35,6 +57,7 @@ export function registerResource(program: Command, spec: ResourceSpec): void { .option("--limit ", "results per page (1-100)", "20") .option("--cursor ", "pagination cursor") .option("--workspace ", "workspace override") + .option("--csv", "CSV output (for spreadsheets/accounting)") .option("--json", "raw JSON output"); for (const f of spec.listFlags ?? []) cmd.option(f.flag, f.describe); cmd.action(async (opts) => { @@ -48,6 +71,10 @@ export function registerResource(program: Command, spec: ResourceSpec): void { if (v !== undefined) query[f.query] = v; } const body = unwrap(await list({ query })); + if (opts.csv) { + process.stdout.write(`${toCsv(body.data, spec.columns)}\n`); + return; + } print(body.data, { json: opts.json, columns: spec.columns }); if (!opts.json) printNextCursor(body.page); }); @@ -57,7 +84,7 @@ export function registerResource(program: Command, spec: ResourceSpec): void { const get = spec.get; root .command("get ") - .description(`Get one ${spec.name.replace(/s$/, "")} by id`) + .description(`Get one ${singular} by id`) .option("--workspace ", "workspace override") .option("--json", "raw JSON output") .action(async (id, opts) => { @@ -66,6 +93,75 @@ export function registerResource(program: Command, spec: ResourceSpec): void { print(body.data, { json: opts.json }); }); } + + if (spec.create) { + const create = spec.create; + root + .command("create") + .description(`Create a ${singular}`) + .requiredOption("--data ", "JSON body (inline or @file.json)") + .option("--workspace ", "workspace override") + .option("--json", "raw JSON output") + .action(async (opts) => { + configureClient(opts.workspace); + const body = unwrap(await create({ body: parseData(opts.data) })); + print(body.data ?? body, { json: opts.json }); + }); + } + + if (spec.update) { + const update = spec.update; + root + .command("update ") + .description(`Update a ${singular} by id`) + .requiredOption("--data ", "JSON body (inline or @file.json)") + .option("--workspace ", "workspace override") + .option("--json", "raw JSON output") + .action(async (id, opts) => { + configureClient(opts.workspace); + const body = unwrap( + await update({ path: { id }, body: parseData(opts.data) }), + ); + print(body.data ?? body, { json: opts.json }); + }); + } + + if (spec.del) { + const del = spec.del; + root + .command("delete ") + .description(`Delete a ${singular} by id`) + .option("--yes", "skip confirmation") + .option("--workspace ", "workspace override") + .option("--json", "raw JSON output") + .action(async (id, opts) => { + if (!opts.yes && !(await confirm(`Delete ${singular} ${id}?`))) { + console.error("Aborted."); + return; + } + configureClient(opts.workspace); + const body = unwrap(await del({ path: { id } })); + print(body?.data ?? { deleted: id }, { json: opts.json }); + }); + } + + for (const action of spec.actions ?? []) { + const cmd = root + .command(`${action.name} `) + .description(action.describe) + .option("--workspace ", "workspace override") + .option("--json", "raw JSON output"); + if (action.body) { + cmd.option("--data ", "JSON body (inline or @file.json)"); + } + cmd.action(async (id, opts) => { + configureClient(opts.workspace); + const payload: Record = { path: { id } }; + if (action.body && opts.data) payload.body = parseData(opts.data); + const body = unwrap(await action.fn(payload)); + print(body?.data ?? body ?? { ok: true, id }, { json: opts.json }); + }); + } } function camel(s: string): string { diff --git a/src/index.ts b/src/index.ts index 492caa1..4fafcbe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,14 @@ import { Command } from "commander"; // Version synced with package.json at build time (tsup injects it via import). import pkg from "../package.json" with { type: "json" }; import { + deleteEventsId, + deleteEventsIdDatesDateId, + deleteMembershipPlansId, + deleteTicketTypesId, + deleteVenuesId, getEvents, getEventsId, + getEventsIdDates, getMembershipPlans, getMembershipPlansId, getSales, @@ -13,8 +19,25 @@ import { getTicketTypesId, getVenues, getVenuesId, + patchEventsId, + patchEventsIdDatesDateId, + patchMembershipPlansId, + patchStaffIdRole, + patchTicketTypesId, + patchVenuesId, + postEvents, + postEventsIdDates, + postEventsIdPublish, + postMembershipPlans, + postSalesIdCancel, + postSalesIdRefund, + postStaff, + postTicketTypes, + postVenues, } from "./client/sdk.gen"; +import { registerAdmin } from "./commands/admin"; import { registerAuth } from "./commands/auth"; +import { registerEventDates } from "./commands/event-dates"; import { registerReports } from "./commands/reports"; import { registerResource } from "./commands/resource"; import { banner } from "./lib/banner"; @@ -34,14 +57,36 @@ registerResource(program, { describe: "Events", list: getEvents, get: getEventsId, + create: postEvents, + update: patchEventsId, + del: deleteEventsId, + actions: [ + { name: "publish", describe: "Publish an event", fn: postEventsIdPublish }, + ], columns: ["id", "name", "status", "startsAt"], }); +registerEventDates(program, { + list: getEventsIdDates, + create: postEventsIdDates, + update: patchEventsIdDatesDateId, + del: deleteEventsIdDatesDateId, +}); + registerResource(program, { name: "sales", describe: "Sales", list: getSales, get: getSalesId, + actions: [ + { name: "cancel", describe: "Cancel a sale", fn: postSalesIdCancel }, + { + name: "refund", + describe: "Refund a sale (--data for partial amount)", + fn: postSalesIdRefund, + body: true, + }, + ], columns: ["id", "reference", "status", "total", "currency", "createdAt"], listFlags: [ { flag: "--status ", describe: "filter by status", query: "status" }, @@ -53,6 +98,9 @@ registerResource(program, { describe: "Ticket types", list: getTicketTypes, get: getTicketTypesId, + create: postTicketTypes, + update: patchTicketTypesId, + del: deleteTicketTypesId, columns: ["id", "name", "price", "currency", "stock"], listFlags: [ { @@ -68,6 +116,9 @@ registerResource(program, { describe: "Membership plans", list: getMembershipPlans, get: getMembershipPlansId, + create: postMembershipPlans, + update: patchMembershipPlansId, + del: deleteMembershipPlansId, columns: ["id", "name", "price", "currency", "interval"], }); @@ -76,6 +127,9 @@ registerResource(program, { describe: "Venues", list: getVenues, get: getVenuesId, + create: postVenues, + update: patchVenuesId, + del: deleteVenuesId, columns: ["id", "name", "city"], }); @@ -83,10 +137,20 @@ registerResource(program, { name: "staff", describe: "Workspace staff", list: getStaff, + create: postStaff, + actions: [ + { + name: "set-role", + describe: 'Change a staff member\'s role (--data \'{"role":"..."}\')', + fn: patchStaffIdRole, + body: true, + }, + ], columns: ["id", "name", "email", "role"], }); registerReports(program); +registerAdmin(program); // No arguments -> banner + help. if (process.argv.length <= 2) { diff --git a/src/lib/api.ts b/src/lib/api.ts index 77d083c..3296f70 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,3 +1,4 @@ +import { client as adminClient } from "../admin-client/client.gen"; import { client } from "../client/client.gen"; import { loadConfig } from "./config"; @@ -20,6 +21,29 @@ export function configureClient(workspaceOverride?: string): void { }); } +/** + * Configures the superadmin client (/api/admin). Auth is a better-auth session + * cookie of a SUPER_ADMIN — NOT an API key (the admin contract issues no keys). + * MVP: paste the session token via FT_ADMIN_SESSION; converges to a service + * token once the backend ships it (see free-admin#157). + * ponytail: cookie header until the Bearer service-token lands. + */ +export function configureAdminClient(): void { + const cfg = loadConfig(); + if (!cfg.adminSession) { + fail( + "No superadmin session. Export FT_ADMIN_SESSION with a SUPER_ADMIN better-auth session token.", + "Log in to the admin panel in your browser and copy the `better-auth.session_token` cookie.", + ); + } + adminClient.setConfig({ + baseUrl: `${cfg.apiUrl.replace(/\/$/, "")}/api/admin`, + headers: { + Cookie: `better-auth.session_token=${cfg.adminSession}`, + }, + }); +} + type ApiError = { code?: string; message?: string; diff --git a/src/lib/config.ts b/src/lib/config.ts index 3c746c0..d277698 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -6,6 +6,8 @@ export interface FtConfig { apiUrl: string; apiKey?: string; workspaceId?: string; + /** Superadmin (/api/admin): better-auth session token of a SUPER_ADMIN. */ + adminSession?: string; } const CONFIG_DIR = join(homedir(), ".freeticket"); @@ -31,6 +33,7 @@ export function loadConfig(): FtConfig { apiUrl: process.env.FT_API_URL ?? file.apiUrl ?? DEFAULT_API_URL, apiKey: process.env.FT_API_KEY ?? file.apiKey, workspaceId: process.env.FT_WORKSPACE_ID ?? file.workspaceId, + adminSession: process.env.FT_ADMIN_SESSION ?? file.adminSession, }; } diff --git a/src/lib/input.ts b/src/lib/input.ts new file mode 100644 index 0000000..9b934ab --- /dev/null +++ b/src/lib/input.ts @@ -0,0 +1,47 @@ +import { readFileSync } from "node:fs"; +import * as readline from "node:readline"; +import { fail } from "./api"; + +/** + * Parses a `--data` value into a JSON body. Accepts inline JSON (`'{"x":1}'`) + * or `@path/to/file.json` to read from disk. Aborts on missing/invalid input. + */ +export function parseData(input?: string): unknown { + if (!input) { + fail( + "Missing --data. Pass inline JSON or @path/to/file.json.", + 'Example: --data \'{"name":"My event"}\' or --data @event.json', + ); + } + let raw = input as string; + if (raw.startsWith("@")) { + try { + raw = readFileSync(raw.slice(1), "utf8"); + } catch { + fail(`Cannot read file: ${raw.slice(1)}`); + } + } + try { + return JSON.parse(raw); + } catch { + return fail("Invalid JSON in --data."); + } +} + +/** + * Y/N confirmation prompt on stderr (keeps stdout clean for `--json`/pipes). + * Returns true only on an explicit yes. Non-interactive stdin → false. + */ +export function confirm(question: string): Promise { + if (!process.stdin.isTTY) return Promise.resolve(false); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stderr, + }); + return new Promise((resolve) => { + rl.question(`${question} [y/N] `, (answer) => { + rl.close(); + resolve(/^y(es)?$/i.test(answer.trim())); + }); + }); +} diff --git a/src/lib/output.ts b/src/lib/output.ts index c24927b..d919e57 100644 --- a/src/lib/output.ts +++ b/src/lib/output.ts @@ -32,6 +32,25 @@ export function print(data: unknown, opts: PrintOpts = {}): void { process.stdout.write(`${JSON.stringify(data, null, 2)}\n`); } +/** + * Serializes an array of flat rows to CSV (RFC 4180 quoting). Columns default + * to the first row's keys. Objects/arrays in cells are JSON-stringified. + */ +export function toCsv(rows: unknown, columns?: string[]): string { + if (!Array.isArray(rows) || rows.length === 0) return ""; + const cols = columns ?? Object.keys(rows[0] as object); + const esc = (v: unknown): string => { + if (v === null || v === undefined) return ""; + const s = typeof v === "object" ? JSON.stringify(v) : String(v); + return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; + }; + const head = cols.map(esc).join(","); + const body = (rows as Record[]).map((r) => + cols.map((c) => esc(r[c])).join(","), + ); + return [head, ...body].join("\n"); +} + /** Pagination hint on stderr to avoid contaminating `--json` output. */ export function printNextCursor(page?: { nextCursor?: string | null }): void { if (page?.nextCursor) {