Implementação MVP

This commit is contained in:
camilafagundes committed 2026-10-03 00:00:45 -03:00
commit 807b3d72cd
54 files changed
+5241

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
/**
* Reserva de número comercial dentro da transação formal.
* O escopo da sequência (workspace, global ou outro) é P-009 / ADR-001 e não está decidido.
* Não há implementação de produção nesta fase.
*/
export interface NumberReservation {
readonly commercialNumber: string;
commit(): void;
rollback(): void;
}
export interface CommercialNumberAllocator {
reserve(input: {
readonly workspaceId: string;
readonly proposalId: string;
}): Promise<NumberReservation>;
}
+18
View File
@@ -0,0 +1,18 @@
export type LockHandle = {
readonly key: string;
readonly ownerId: string;
readonly token: string;
};
/**
* Lock distribuído. A implementação de produção será Redis.
* Não existe operação de tomada forçada.
*/
export interface DistributedLockService {
acquire(input: {
readonly key: string;
readonly ttlMs: number;
readonly ownerId: string;
}): Promise<LockHandle | null>;
release(handle: LockHandle): Promise<boolean>;
}
+24
View File
@@ -0,0 +1,24 @@
import type { CommittedGeneration, GenerationRequest } from "../domain/generation/types";
import type { CommercialNumberAllocator } from "./commercial-number-allocator";
import type { StagingArtifactStore } from "./staging-artifact";
export type FormalGenerationIdentity = {
readonly workspaceId: string;
readonly proposalId: string;
readonly idempotencyKey: string;
};
/**
* Fonte formal da geração. Em produção, uma transação PostgreSQL.
* Número e versão só persistem se o artefato promovido entrar no mesmo commit.
* Não há update nem delete de versão formal.
*/
export interface FormalGenerationStore {
findByIdempotency(identity: FormalGenerationIdentity): Promise<CommittedGeneration | null>;
commit(input: {
readonly request: GenerationRequest;
readonly stagingRef: string;
readonly numbers: CommercialNumberAllocator;
readonly staging: StagingArtifactStore;
}): Promise<CommittedGeneration>;
}
+15
View File
@@ -0,0 +1,15 @@
import type { FormalGenerationIdentity } from "./formal-generation-store";
export type GenerationClaim = "claimed" | "same-in-progress" | "conflict";
/**
* Estado efêmero do job. Em produção, Redis.
* PostgreSQL continua sendo a fonte do resultado formal.
*/
export interface GenerationAttemptLedger {
claim(input: FormalGenerationIdentity & {
readonly fingerprint: string;
readonly ttlMs: number;
}): Promise<GenerationClaim>;
release(identity: FormalGenerationIdentity): Promise<void>;
}
+5
View File
@@ -0,0 +1,5 @@
import type { GenerationOutcome, GenerationRequest } from "../domain/generation/types";
export interface GenerationCoordinator {
generate(request: GenerationRequest): Promise<GenerationOutcome>;
}
+26
View File
@@ -0,0 +1,26 @@
import type { CorrelationContext } from "../domain/shared/correlation";
import type { ReplyToPolicy } from "../domain/delivery";
export type ProposalEmailCommand = {
readonly workspaceId: string;
readonly proposalVersionId: string;
readonly pdfRef: string;
readonly toAddress: string;
readonly subject: string;
readonly body: string;
readonly replyTo: ReplyToPolicy;
readonly correlation: CorrelationContext;
};
export type ProposalEmailResult =
| { readonly status: "blocked"; readonly pending: "P-001" | "P-011" }
| { readonly status: "accepted"; readonly providerMessageId: string }
| { readonly status: "failed"; readonly providerMessageId: null };
/**
* P-011 / ADR-003: não há provider escolhido.
* O PDF enviado tem de ser o `pdfRef` da versão exibida. Este port não escolhe outra versão.
*/
export interface ProposalEmailProvider {
send(command: ProposalEmailCommand): Promise<ProposalEmailResult>;
}
+22
View File
@@ -0,0 +1,22 @@
import type { CorrelationContext } from "../domain/shared/correlation";
import type { RenderedStagingArtifact } from "./staging-artifact";
export type ProposalPdfRenderInput = {
readonly workspaceId: string;
readonly proposalId: string;
readonly modelVersionId: string;
readonly contentHash: string;
readonly correlation: CorrelationContext;
};
export type ProposalPdfRenderResult =
| ({ readonly status: "rendered" } & RenderedStagingArtifact)
| { readonly status: "failed"; readonly code: "render-failed" };
/**
* Composição do PDF a partir de representação normalizada.
* A stack prevê `@react-pdf/renderer` no adapter futuro. O adapter real não existe nesta fase.
*/
export interface ProposalPdfRenderer {
render(input: ProposalPdfRenderInput): Promise<ProposalPdfRenderResult>;
}
+13
View File
@@ -0,0 +1,13 @@
export type RenderedStagingArtifact = {
readonly stagingRef: string;
readonly contentHash: string;
};
export interface StagingArtifactStore {
/**
* Move o artefato de staging para uma referência formal.
* Só pode ficar visível se o commit da geração concluir.
*/
promote(stagingRef: string): Promise<{ readonly pdfRef: string }>;
discard(stagingRef: string): Promise<void>;
}
+28
View File
@@ -0,0 +1,28 @@
import type { CorrelationContext } from "../domain/shared/correlation";
import type { FileSecurityPolicy } from "../domain/shared/file-security-policy";
export type TemplateMediaType =
| "application/pdf"
| "application/vnd.openxmlformats-officedocument.presentationml.presentation"
| "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
export type TemplateIngestionResult =
| { readonly status: "blocked"; readonly pending: "P-006" | "P-010" }
| {
readonly status: "extracted";
readonly markers: readonly { readonly fieldKey: string; readonly occurrenceIndex: number }[];
};
/**
* P-010 / ADR-002: não há motor escolhido.
* Marcadores continuam sendo somente `[campo]` explícito. Este port não infere campos.
*/
export interface TemplateIngestionService {
ingest(input: {
readonly workspaceId: string;
readonly sourceFileRef: string;
readonly mediaType: TemplateMediaType;
readonly policy: FileSecurityPolicy;
readonly correlation: CorrelationContext;
}): Promise<TemplateIngestionResult>;
}