Implementação MVP
This commit is contained in:
commit
807b3d72cd
54 files changed
+5241
No files matched your search
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { GenerationOutcome, GenerationRequest } from "../domain/generation/types";
|
||||
|
||||
export interface GenerationCoordinator {
|
||||
generate(request: GenerationRequest): Promise<GenerationOutcome>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
Reference in new issue
Block a user