import fs from 'node:fs'; const ref = name => ({ $ref: `#/components/schemas/${name}` }); const id = { name: 'id', in: 'path', required: true, schema: { type: 'string', format: 'uuid' } }; const responses = schema => Object.fromEntries(['200', '401', '403', '404', '409', '422', '429'].map(status => [status, { description: { '200': 'Private response; Cache-Control private, no-store', '401': 'Authentication required', '403': 'Account, ownership or current reviewer/MFA policy rejected', '404': 'Module disabled or resource unavailable', '409': 'Stale version or invalid transition', '422': 'Validation error', '429': 'Rate limited' }[status], ...(status === '200' ? { content: { 'application/json': { schema } } } : {}), }])); const operation = (summary, response, body, parameters = []) => ({ summary, parameters, security: [{ session: [] }], responses: responses(response), ...(body ? { requestBody: { required: true, content: { 'application/json': { schema: body } } }, description: 'Session authentication and Laravel CSRF/origin protection apply. Use X-XSRF-TOKEN for browser clients.' } : {}) }); const text = { type: 'string' }; const payload = { type: 'object', additionalProperties: false, required: ['title'], properties: { title: { type: 'string', maxLength: 200 }, description: { type: ['string', 'null'], maxLength: 10000 }, declared_reason: { type: ['string', 'null'], maxLength: 2000 }, source_reference: { type: ['string', 'null'], maxLength: 2000 }, valid_until: { type: ['string', 'null'], format: 'date' } } }; const input = { type: 'object', required: ['modality', 'classification', 'reason', 'payload'], properties: { modality: text, classification: { enum: ['internal', 'confidential', 'restricted'] }, reason: { type: 'string', minLength: 10, maxLength: 1000 }, payload } }; const document = { openapi: '3.1.0', info: { title: 'InvestMoz — private existing-business intake', version: '1.0.0', description: 'Internal preparation and review only. No publication, certification, legal transfer, NDA, payment or external CRM operation.' }, servers: [{ url: '/api/v1/existing-business' }], paths: { '/listings': { get: operation('List own active records, 20 per page', { type: 'object', required: ['records', 'management', 'modalities'], properties: { records: { type: 'object', properties: { data: { type: 'array', items: ref('Business') }, total: { type: 'integer' } } }, management: { type: 'boolean' }, modalities: { type: 'array', items: text } } }, null, [{ name: 'page', in: 'query', schema: { type: 'integer', minimum: 1 } }]), post: operation('Create private draft', ref('Business'), input), }, '/reviews': { get: operation('List submitted records for designated reviewers with MFA; excludes own records', { type: 'object', properties: { records: { type: 'object', properties: { data: { type: 'array', items: ref('Business') } } }, management: { type: 'boolean' } } }) }, '/listings/{id}': { get: operation('Read own record or currently reviewable submission', { type: 'object', properties: { record: ref('Business'), canEdit: { type: 'boolean' }, owner: { type: 'boolean' }, canReview: { type: 'boolean' }, history: { type: 'array', items: { type: 'object', properties: { version_no: { type: 'integer' }, action: text, created_at: text, note: text } } } } }, null, [id]), put: operation('Update owned draft or request for information; optimistic version required', ref('Business'), { ...input, required: [...input.required, 'version_no'], properties: { ...input.properties, version_no: { type: 'integer', minimum: 1 } } }, [id]), }, '/listings/{id}/transitions': { post: operation('Submit, begin review, request information or withdraw', ref('Business'), { type: 'object', required: ['version_no', 'action', 'note'], properties: { version_no: { type: 'integer', minimum: 1 }, action: { enum: ['submitted', 'in_review', 'needs_information', 'withdrawn'] }, note: { type: 'string', minLength: 10, maxLength: 2000 }, review_consent: { type: 'boolean', description: 'Must be true for submission. Stored in encrypted review evidence. Does not authorize publication.' } } }, [id]) }, }, components: { securitySchemes: { session: { type: 'apiKey', in: 'cookie', name: 'session_cookie', description: 'Actual Laravel cookie name comes from deployment configuration; no bearer token support.' } }, schemas: { Business: { type: 'object', required: ['id', 'code', 'modality', 'status', 'classification', 'version_no', 'payload'], properties: { id: { type: 'string', format: 'uuid' }, code: text, modality: text, status: { enum: ['draft', 'submitted', 'in_review', 'needs_information', 'withdrawn'] }, classification: { enum: ['internal', 'confidential', 'restricted'] }, version_no: { type: 'integer', minimum: 1 }, payload } } } }, }; document.paths['/listings'].post.responses['201'] = document.paths['/listings'].post.responses['200']; delete document.paths['/listings'].post.responses['200']; const objectPayload = { type: 'object', additionalProperties: false, required: ['label', 'source_reference'], properties: Object.fromEntries(['label','declared_holder','document_reference','source_reference','competent_entity','purpose','location_description','boundaries_description','occupancy','condition','encumbrances','restrictions','required_authorizations','transfer_condition','contract_type','valid_from','valid_until','area','area_unit','amount','currency','value_kind','participation_percent'].map(key => [key, { type: ['string','null'] }])) }; const objectInput = { type: 'object', required: ['kind','version_no','reason','payload'], properties: { kind: { enum: ['company','participation','asset','property','unit','improvement','agricultural_investment','land_right','contract','license','authorization','encumbrance','valuation','capital_request'] }, version_no: { type: 'integer', minimum: 1 }, reason: { type: 'string', minLength: 10, maxLength: 1000 }, payload: objectPayload } }; const objectResponse = { type: 'object', properties: { id: { type: 'string', format: 'uuid' }, version_no: { type: 'integer' }, verification_status: { const: 'pending' } } }; document.paths['/listings/{id}/objects'] = { post: operation('Declare a separate restricted object; increments aggregate version without granting verification', objectResponse, objectInput, [id]) }; document.paths['/listings/{id}/objects'].post.responses['201'] = document.paths['/listings/{id}/objects'].post.responses['200']; delete document.paths['/listings/{id}/objects'].post.responses['200']; document.paths['/listings/{id}/objects/{object}'] = { put: operation('Update scoped object with immutable kind; amount requires currency and value kind; area requires unit', objectResponse, objectInput, [id, { ...id, name: 'object' }]) }; const verification = { type: 'object', required: ['version_no', 'check', 'outcome', 'evidence_reference', 'note'], properties: { version_no: { type: 'integer', minimum: 1 }, object_id: { type: ['string', 'null'], format: 'uuid' }, check: { enum: ['identity', 'representation', 'declared_ownership', 'documentary_ownership', 'document_received', 'document_validated', 'independent_valuation', 'land_right_documented', 'license_confirmed', 'validity', 'legal_transfer', 'prior_authorization'] }, outcome: { enum: ['confirmed', 'rejected', 'inconclusive', 'revoked'] }, evidence_reference: { type: 'string', minLength: 10, maxLength: 2000 }, note: { type: 'string', minLength: 10, maxLength: 2000 }, valid_until: { type: ['string', 'null'], format: 'date', description: 'Required for confirmed findings; must be today or later.' }, } }; document.paths['/listings/{id}/verifications'] = { post: operation('Record independent finding while in_review; immutable encrypted evidence, optimistic version and scoped object. Does not publish or certify the business.', ref('Business'), verification, [id]) }; document.paths['/listings/{id}'].get.responses['200'].content['application/json'].schema.properties.verifications = { type: 'array', items: { ...verification, properties: { ...verification.properties, effective_outcome: { enum: ['confirmed', 'rejected', 'inconclusive', 'revoked', 'stale', 'expired'] }, content_version: { type: 'integer' }, created_at: text } }, }; const formSchema = JSON.parse(fs.readFileSync('docs/investmoz/existing-business/form-schema.json', 'utf8')); for (const [key, field] of Object.entries(formSchema)) { payload.properties[key] = { type: key === 'title' ? 'string' : ['string', 'null'], maxLength: field.max, ...(field.options ? { enum: [...Object.keys(field.options), null] } : {}), ...(field.type === 'date' ? { format: 'date' } : {}), description: field.en }; } payload.properties.wizard_version = { type: ['string', 'null'], enum: ['1', null] }; payload.properties.wizard_step = { type: ['string', 'null'], pattern: '^[1-8]$' }; input.properties.sector_id = { type: ['integer', 'null'], description: 'Existing active public.sectors identifier; required on wizard submission.' }; document.paths['/listings/{id}'].put.requestBody.content['application/json'].schema.properties.sector_id = input.properties.sector_id; document.components.schemas.Business.properties.sector_id = input.properties.sector_id; document.paths['/listings/{id}/transitions'].post.requestBody.content['application/json'].schema.properties.confirmation_code = { type: ['string', 'null'], description: 'Exact business code required for wizard_version=1 submission, in addition to internal review consent.' }; document.info.version = '1.2.0'; fs.writeFileSync('docs/investmoz/existing-business/openapi.json', JSON.stringify(document, null, 2) + '\n'); console.log('OpenAPI written: 9 operations, private session API.');