Files
Investidor-V2/tools/existing-business-openapi.mjs
nala 1106509116
tests / ci (push) Canceled after 0s
vrsao 2
2026-10-08 14:02:01 +02:00

444 lines
15 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', '2', 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.');