64 lines
9.6 KiB
JavaScript
64 lines
9.6 KiB
JavaScript
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.');
|