openapi: 3.0.3 info: title: ERP Construccion API description: | API REST para sistema de administracion de obra e INFONAVIT. ## Autenticacion La API utiliza JWT Bearer tokens. Incluir el header: ``` Authorization: Bearer ``` ## Multi-tenancy Todas las operaciones estan aisladas por tenant_id. El tenant se determina automaticamente desde el JWT. ## Paginacion Los endpoints de listado soportan paginacion: - `page`: Numero de pagina (default: 1) - `limit`: Items por pagina (default: 20, max: 100) version: 1.0.0 contact: name: ERP Construccion Team email: dev@construccion.local servers: - url: http://localhost:3000/api/v1 description: Development - url: https://api.construccion.example.com/api/v1 description: Production tags: - name: Auth description: Autenticacion y autorizacion - name: Conceptos description: Catalogo de conceptos de obra - name: Presupuestos description: Presupuestos de obra - name: Avances description: Control de avances fisicos - name: Bitacora description: Bitacora de obra - name: Estimaciones description: Estimaciones periodicas paths: /auth/login: post: tags: [Auth] summary: Login de usuario operationId: login requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LoginRequest' responses: '200': description: Login exitoso content: application/json: schema: $ref: '#/components/schemas/AuthResponse' '401': $ref: '#/components/responses/Unauthorized' /auth/register: post: tags: [Auth] summary: Registro de usuario operationId: register requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RegisterRequest' responses: '201': description: Usuario registrado content: application/json: schema: $ref: '#/components/schemas/AuthResponse' '400': $ref: '#/components/responses/BadRequest' /auth/refresh: post: tags: [Auth] summary: Renovar access token operationId: refreshToken requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RefreshTokenRequest' responses: '200': description: Tokens renovados content: application/json: schema: $ref: '#/components/schemas/AuthResponse' '401': $ref: '#/components/responses/Unauthorized' /auth/logout: post: tags: [Auth] summary: Logout (revocar refresh token) operationId: logout security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RefreshTokenRequest' responses: '204': description: Logout exitoso /conceptos: get: tags: [Conceptos] summary: Listar conceptos operationId: listConceptos security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' responses: '200': description: Lista de conceptos content: application/json: schema: $ref: '#/components/schemas/ConceptoListResponse' post: tags: [Conceptos] summary: Crear concepto operationId: createConcepto security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateConceptoRequest' responses: '201': description: Concepto creado content: application/json: schema: $ref: '#/components/schemas/Concepto' /conceptos/tree: get: tags: [Conceptos] summary: Obtener arbol de conceptos operationId: getConceptoTree security: - bearerAuth: [] parameters: - name: rootId in: query schema: type: string format: uuid description: ID del concepto raiz (opcional) responses: '200': description: Arbol de conceptos content: application/json: schema: type: array items: $ref: '#/components/schemas/ConceptoTree' /conceptos/{id}: get: tags: [Conceptos] summary: Obtener concepto por ID operationId: getConcepto security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Concepto encontrado content: application/json: schema: $ref: '#/components/schemas/Concepto' '404': $ref: '#/components/responses/NotFound' /presupuestos: get: tags: [Presupuestos] summary: Listar presupuestos operationId: listPresupuestos security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' - name: fraccionamientoId in: query schema: type: string format: uuid responses: '200': description: Lista de presupuestos content: application/json: schema: $ref: '#/components/schemas/PresupuestoListResponse' post: tags: [Presupuestos] summary: Crear presupuesto operationId: createPresupuesto security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreatePresupuestoRequest' responses: '201': description: Presupuesto creado content: application/json: schema: $ref: '#/components/schemas/Presupuesto' /presupuestos/{id}: get: tags: [Presupuestos] summary: Obtener presupuesto con partidas operationId: getPresupuesto security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Presupuesto con partidas content: application/json: schema: $ref: '#/components/schemas/PresupuestoDetalle' /presupuestos/{id}/partidas: post: tags: [Presupuestos] summary: Agregar partida al presupuesto operationId: addPartida security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AddPartidaRequest' responses: '201': description: Partida agregada content: application/json: schema: $ref: '#/components/schemas/PresupuestoPartida' /presupuestos/{id}/approve: post: tags: [Presupuestos] summary: Aprobar presupuesto operationId: approvePresupuesto security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Presupuesto aprobado content: application/json: schema: $ref: '#/components/schemas/Presupuesto' /avances: get: tags: [Avances] summary: Listar avances operationId: listAvances security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' - name: loteId in: query schema: type: string format: uuid - name: status in: query schema: $ref: '#/components/schemas/AdvanceStatus' responses: '200': description: Lista de avances content: application/json: schema: $ref: '#/components/schemas/AvanceListResponse' post: tags: [Avances] summary: Crear avance operationId: createAvance security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateAvanceRequest' responses: '201': description: Avance creado content: application/json: schema: $ref: '#/components/schemas/AvanceObra' /avances/{id}/fotos: post: tags: [Avances] summary: Agregar foto al avance operationId: addFotoAvance security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/IdParam' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AddFotoRequest' responses: '201': description: Foto agregada content: application/json: schema: $ref: '#/components/schemas/FotoAvance' /avances/{id}/approve: post: tags: [Avances] summary: Aprobar avance operationId: approveAvance security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Avance aprobado content: application/json: schema: $ref: '#/components/schemas/AvanceObra' /estimaciones: get: tags: [Estimaciones] summary: Listar estimaciones operationId: listEstimaciones security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/PageParam' - $ref: '#/components/parameters/LimitParam' - name: contratoId in: query schema: type: string format: uuid - name: status in: query schema: $ref: '#/components/schemas/EstimateStatus' responses: '200': description: Lista de estimaciones content: application/json: schema: $ref: '#/components/schemas/EstimacionListResponse' post: tags: [Estimaciones] summary: Crear estimacion operationId: createEstimacion security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateEstimacionRequest' responses: '201': description: Estimacion creada content: application/json: schema: $ref: '#/components/schemas/Estimacion' /estimaciones/{id}/submit: post: tags: [Estimaciones] summary: Enviar estimacion para revision operationId: submitEstimacion security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Estimacion enviada content: application/json: schema: $ref: '#/components/schemas/Estimacion' /estimaciones/{id}/approve: post: tags: [Estimaciones] summary: Aprobar estimacion operationId: approveEstimacion security: - bearerAuth: [] parameters: - $ref: '#/components/parameters/IdParam' responses: '200': description: Estimacion aprobada content: application/json: schema: $ref: '#/components/schemas/Estimacion' components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT parameters: IdParam: name: id in: path required: true schema: type: string format: uuid PageParam: name: page in: query schema: type: integer minimum: 1 default: 1 LimitParam: name: limit in: query schema: type: integer minimum: 1 maximum: 100 default: 20 responses: BadRequest: description: Solicitud invalida content: application/json: schema: $ref: '#/components/schemas/Error' Unauthorized: description: No autorizado content: application/json: schema: $ref: '#/components/schemas/Error' NotFound: description: Recurso no encontrado content: application/json: schema: $ref: '#/components/schemas/Error' schemas: Error: type: object properties: error: type: string message: type: string PaginationMeta: type: object properties: total: type: integer page: type: integer limit: type: integer totalPages: type: integer LoginRequest: type: object required: [email, password] properties: email: type: string format: email password: type: string minLength: 8 tenantId: type: string format: uuid RegisterRequest: type: object required: [email, password, firstName, lastName, tenantId] properties: email: type: string format: email password: type: string minLength: 8 firstName: type: string lastName: type: string tenantId: type: string format: uuid RefreshTokenRequest: type: object required: [refreshToken] properties: refreshToken: type: string AuthResponse: type: object properties: accessToken: type: string refreshToken: type: string expiresIn: type: integer user: type: object properties: id: type: string format: uuid email: type: string firstName: type: string lastName: type: string roles: type: array items: type: string tenant: type: object properties: id: type: string format: uuid name: type: string Concepto: type: object properties: id: type: string format: uuid code: type: string name: type: string description: type: string parentId: type: string format: uuid level: type: integer path: type: string unitPrice: type: number isComposite: type: boolean createdAt: type: string format: date-time ConceptoTree: allOf: - $ref: '#/components/schemas/Concepto' - type: object properties: children: type: array items: $ref: '#/components/schemas/ConceptoTree' CreateConceptoRequest: type: object required: [code, name] properties: code: type: string maxLength: 50 name: type: string maxLength: 255 description: type: string parentId: type: string format: uuid unitPrice: type: number isComposite: type: boolean ConceptoListResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/Concepto' meta: $ref: '#/components/schemas/PaginationMeta' Presupuesto: type: object properties: id: type: string format: uuid code: type: string name: type: string version: type: integer isActive: type: boolean totalAmount: type: number approvedAt: type: string format: date-time createdAt: type: string format: date-time PresupuestoPartida: type: object properties: id: type: string format: uuid conceptoId: type: string format: uuid quantity: type: number unitPrice: type: number totalAmount: type: number sequence: type: integer PresupuestoDetalle: allOf: - $ref: '#/components/schemas/Presupuesto' - type: object properties: partidas: type: array items: $ref: '#/components/schemas/PresupuestoPartida' CreatePresupuestoRequest: type: object required: [code, name] properties: code: type: string name: type: string description: type: string fraccionamientoId: type: string format: uuid prototipoId: type: string format: uuid AddPartidaRequest: type: object required: [conceptoId, quantity, unitPrice] properties: conceptoId: type: string format: uuid quantity: type: number unitPrice: type: number sequence: type: integer PresupuestoListResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/Presupuesto' meta: $ref: '#/components/schemas/PaginationMeta' AdvanceStatus: type: string enum: [pending, captured, reviewed, approved, rejected] AvanceObra: type: object properties: id: type: string format: uuid loteId: type: string format: uuid conceptoId: type: string format: uuid captureDate: type: string format: date quantityExecuted: type: number percentageExecuted: type: number status: $ref: '#/components/schemas/AdvanceStatus' notes: type: string createdAt: type: string format: date-time FotoAvance: type: object properties: id: type: string format: uuid fileUrl: type: string fileName: type: string description: type: string capturedAt: type: string format: date-time CreateAvanceRequest: type: object required: [conceptoId, captureDate, quantityExecuted] properties: loteId: type: string format: uuid departamentoId: type: string format: uuid conceptoId: type: string format: uuid captureDate: type: string format: date quantityExecuted: type: number percentageExecuted: type: number notes: type: string AddFotoRequest: type: object required: [fileUrl] properties: fileUrl: type: string fileName: type: string description: type: string location: type: object properties: lat: type: number lng: type: number AvanceListResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/AvanceObra' meta: $ref: '#/components/schemas/PaginationMeta' EstimateStatus: type: string enum: [draft, submitted, reviewed, approved, invoiced, paid, rejected, cancelled] Estimacion: type: object properties: id: type: string format: uuid estimateNumber: type: string contratoId: type: string format: uuid periodStart: type: string format: date periodEnd: type: string format: date sequenceNumber: type: integer status: $ref: '#/components/schemas/EstimateStatus' subtotal: type: number advanceAmount: type: number retentionAmount: type: number taxAmount: type: number totalAmount: type: number approvedAt: type: string format: date-time createdAt: type: string format: date-time CreateEstimacionRequest: type: object required: [contratoId, fraccionamientoId, periodStart, periodEnd] properties: contratoId: type: string format: uuid fraccionamientoId: type: string format: uuid periodStart: type: string format: date periodEnd: type: string format: date notes: type: string EstimacionListResponse: type: object properties: data: type: array items: $ref: '#/components/schemas/Estimacion' meta: $ref: '#/components/schemas/PaginationMeta'