import fs from 'node:fs'; const routes = JSON.parse( fs .readFileSync( process.argv[2] ?? 'C:/Users/macbook/AppData/Local/Temp/zones-routes.json', 'utf8', ) .replace(/^\uFEFF/, ''), ); const string = { type: 'string' }; const uuid = { type: 'string', format: 'uuid' }; const date = { type: 'string', format: 'date' }; const version = { type: 'integer', minimum: 1 }; const nullable = (schema) => ({ ...schema, nullable: true }); const object = (properties, required = []) => ({ type: 'object', properties, required, }); const relation = { association_type: { type: 'string', enum: ['official', 'influence', 'proximity', 'potential', 'historical'], }, role: { type: 'string', enum: [ 'primary', 'beneficiary', 'served', 'enabled', 'dependent', 'related', 'potential', 'historical', ], }, source_reference: { type: 'string', minLength: 10, maxLength: 2000 }, justification: { type: 'string', minLength: 10, maxLength: 3000 }, legal_source_id: nullable(uuid), valid_from: date, valid_until: nullable(date), priority: { type: 'integer', minimum: 0, maximum: 100 }, is_primary: { type: 'boolean' }, }; const relationRequired = [ 'association_type', 'role', 'source_reference', 'justification', 'valid_from', ]; const schemas = { Version: object({ version_no: version }, ['version_no']), Zone: object( { version_no: version, code: { type: 'string', maxLength: 80 }, category: { type: 'string', enum: ['zee', 'zfi', 'industrial_park', 'pending'], }, physical_category: { type: 'string', enum: [ 'economic_zone', 'industrial_park', 'agro_industrial_park', 'industrial_logistics_park', 'industrial_free_zone', 'integrated_tourism_area', 'other', 'unknown', ], }, legal_regime: { type: 'string', enum: ['zee', 'zfi', 'general', 'pending', 'not_applicable'], }, development_status: { type: 'string', enum: [ 'announced', 'planned', 'in_development', 'partially_operational', 'operational', 'suspended', 'unknown', ], }, availability_status: { type: 'string', enum: [ 'confirmed', 'unavailable', 'reported_unverified', 'not_informed', ], }, area_components: { type: 'array', items: { type: 'object' } }, source_evidence: { type: 'array', items: { type: 'object' } }, name: { type: 'string', maxLength: 255 }, name_en: nullable(string), description: nullable(string), description_en: nullable(string), province: nullable(string), district: nullable(string), operator_id: nullable(uuid), legal_source_id: nullable(uuid), }, ['code', 'category', 'name'], ), LegalSource: object( { title: string, url: { type: 'string', format: 'uri', description: 'HTTPS reference only; no remote download.', }, diploma: string, article: string, competent_entity: string, valid_from: date, valid_until: nullable(date), }, [ 'title', 'url', 'diploma', 'article', 'competent_entity', 'valid_from', ], ), Link: object( { opportunity_id: { type: 'integer', minimum: 1 }, ...relation }, ['opportunity_id', ...relationRequired], ), Relation: object( { cluster_id: nullable(uuid), corridor_id: nullable(uuid), project_id: nullable(uuid), ...relation, }, relationRequired, ), Geometry: object( { geometry: object( { type: { type: 'string', enum: [ 'Point', 'LineString', 'Polygon', 'MultiPolygon', ], }, coordinates: { type: 'array', items: {} }, }, ['type', 'coordinates'], ), purpose: { type: 'string', enum: [ 'official_perimeter', 'influence', 'site', 'alternative', ], }, legal_source_id: nullable(uuid), source_reference: string, validating_entity: string, observed_at: date, }, [ 'geometry', 'purpose', 'source_reference', 'validating_entity', 'observed_at', ], ), }; schemas.Zone.description = 'PUT requires version_no. Save withdraws publication and requires a new independent review. No official status inferred from category.'; schemas.Relation.description = 'Exactly one of cluster_id, corridor_id, project_id. Official association requires legal_source_id. Unique zone-target pair; duplicate returns 409.'; schemas.Link.description = 'Official association requires legal_source_id. Creates or reuses UUID mapping for existing opportunity; duplicate zone-item pair returns 409. Potential remains potential after review.'; schemas.Geometry.description = 'GeoJSON geometry in EPSG:4326, 2D, valid, nonempty, max 200000 encoded bytes. Official perimeter requires Polygon/MultiPolygon and legal source. No status inferred from geometry.'; const paths = {}; let operations = 0; for (const route of routes) { const path = '/' + route.uri; const method = route.method.split('|')[0].toLowerCase(); const admin = path.includes('/admin/'); const parameters = [...path.matchAll(/\{([^}]+)\}/g)].map((match) => ({ name: match[1], in: 'path', required: true, schema: match[1] === 'action' ? { type: 'string', enum: path.includes('zone-attachments') ? ['review', 'select-public', 'withdraw-public'] : path.includes('/admin/zones/') ? ['publish', 'suspend'] : ['validate', 'revoke'], } : uuid, })); if ( method === 'get' && (path.endsWith('/zones') || path.endsWith('/zones/export.csv')) ) parameters.push( { name: 'q', in: 'query', schema: { type: 'string', maxLength: 120 }, }, { name: 'category', in: 'query', schema: { type: 'string', enum: ['zee', 'zfi', 'industrial_park'], }, }, { name: 'physical_category', in: 'query', schema: string }, { name: 'legal_regime', in: 'query', schema: string }, { name: 'development_status', in: 'query', schema: string }, { name: 'availability_status', in: 'query', schema: string }, { name: 'activity', in: 'query', schema: string }, { name: 'province_id', in: 'query', schema: uuid }, { name: 'district_id', in: 'query', schema: uuid }, { name: 'admin_post_id', in: 'query', schema: uuid }, { name: 'locality_id', in: 'query', schema: uuid }, { name: 'page', in: 'query', schema: version }, ); if (path.endsWith('/zone-map')) parameters.push( { name: 'zone_id', in: 'query', schema: uuid }, { name: 'bbox[]', in: 'query', style: 'form', explode: true, schema: { type: 'array', minItems: 4, maxItems: 4, items: { type: 'number' }, }, description: 'west,south,east,north; no antimeridian wrap.', }, ); const creates = method === 'post' && !path.includes('{action}'); const privateRoute = admin || path.includes('/management/'); const operation = { operationId: route.action.split('\\').at(-1).replace('@', '_') + '_' + operations++, summary: route.action.split('\\').at(-1), parameters, security: privateRoute ? [{ laravelSession: [] }] : [], responses: { [creates ? '201' : '200']: { description: 'Success. JSON; paginated lists. Private management responses use Cache-Control: private, no-store.', content: { 'application/json': { schema: { type: 'object' } } }, }, 403: { description: 'Missing manager/MFA/designated role or self-review.', }, 404: { description: 'Not found or not publicly visible.' }, 409: { description: 'Stale version or duplicate relation.' }, 419: { description: 'Session/CSRF validation failed.' }, 422: { description: 'Invalid data, source, geometry, overlap or workflow.', }, 429: { description: 'Rate limit exceeded.' }, }, }; if (path.endsWith('/zones/export.csv')) operation.responses['200'] = { description: 'CSV filtered with the same server-side criteria as the public zone list.', content: { 'text/csv': { schema: { type: 'string', format: 'binary' } }, }, }; if (method !== 'get') { const schema = path.includes('{action}') ? 'Version' : path.endsWith('/legal-sources') ? 'LegalSource' : path.endsWith('/links') ? 'Link' : path.endsWith('/relations') ? 'Relation' : path.endsWith('/geometries') ? 'Geometry' : 'Zone'; if (path.endsWith('/attachments')) { operation.requestBody = { required: true, content: { 'multipart/form-data': { schema: object( { file: { type: 'string', format: 'binary' }, kind: { type: 'string', enum: [ 'legal_instrument', 'map', 'photo', 'study', 'other', ], }, caption: { type: 'string', maxLength: 255 }, source_reference: { type: 'string', minLength: 5, maxLength: 2000, }, source_date: nullable(date), rights_status: { type: 'string', enum: [ 'pending', 'confirmed', 'restricted', ], }, rights_basis: nullable(string), }, [ 'file', 'kind', 'caption', 'source_reference', 'rights_status', ], ), }, }, }; } else if (path.includes('zone-attachments/{id}/{action}')) { operation.requestBody = { required: true, content: { 'application/json': { schema: object( { version_no: version, decision: { type: 'string', enum: ['reviewed', 'rejected'], }, rights_status: { type: 'string', enum: [ 'pending', 'confirmed', 'restricted', ], }, rights_basis: nullable(string), personal_data_status: { type: 'string', enum: ['none', 'present', 'unknown'], }, }, path.endsWith('/review') ? [ 'version_no', 'decision', 'rights_status', 'personal_data_status', ] : ['version_no'], ), }, }, }; } else { operation.requestBody = { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/' + schema }, }, }, }; } } (paths[path] ??= {})[method] = operation; } const doc = { openapi: '3.0.3', info: { title: 'InvestMoz zone foundation API', version: '2026-09-28', description: 'Implemented local routes only. Session cookies and CSRF; no bearer protocol invented. Admin: verified manager with confirmed MFA; legal/GIS/publish actions also require configured designated role. 30 requests/minute for management; public API uses application throttle. Approval in the portal is documentary review, not an administrative act. API v1 remains compatible and adds optional zee_id/zfi_id/park_id filters. Response schemas are deliberately open pending full contract typing.', }, paths, components: { securitySchemes: { laravelSession: { type: 'apiKey', in: 'cookie', name: 'laravel_session', description: 'Actual session cookie name is configured by Laravel. Send X-XSRF-TOKEN from the session; no credentials embedded.', }, }, schemas, }, }; fs.writeFileSync( 'docs/investmoz/zones/openapi.json', JSON.stringify(doc, null, 2) + '\n', ); console.log( JSON.stringify({ documented_operations: operations, registered_routes: routes.length, scope: 'Route/method coverage; not exhaustive response-schema conformance.', }), );