Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 125 additions & 87 deletions docs/demo-seed-fixture-manifest.md

Large diffs are not rendered by default.

370 changes: 191 additions & 179 deletions docs/demo-seed.md

Large diffs are not rendered by default.

27 changes: 22 additions & 5 deletions docs/deployment-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,14 @@ DB pool은 기본 최대 10개입니다. 클러스터 규모에 따라 `DB_MAX_P
```bash
export DEMO_DB_PASSWORD='local-demo-password'
export JWT_SECRET_BASE64="$(openssl rand -base64 32)"
export DEMO_SEED_ENABLED=true
export DEMO_SEED_ADMIN_PASSWORD='로컬 전용 12자 이상 값'
docker compose -f compose.demo.yml up --build
```

PostgreSQL Demo Seed는 #94 검증이 끝날 때까지 기본적으로 꺼져 있습니다. 실행 후
`POST /api/v1/auth/signup`으로 가상 사업장 계정을 만들거나, #94 완료 뒤에만
`DEMO_SEED_ENABLED=true`와 `DEMO_SEED_ADMIN_PASSWORD`를 추가합니다.
`DEMO_SEED_ENABLED`의 Compose 기본값은 안전하게 `false`입니다. 개인 Demo DB에서 Seed가
필요한 경우에만 위와 같이 활성화하고 12자 이상의 합성 비밀번호를 지정합니다. 첫 기동은
빈 PostgreSQL 17 DB에 Flyway와 전체 Demo Seed를 적용합니다.

확인:

Expand All @@ -92,8 +94,23 @@ curl --fail http://127.0.0.1:8080/actuator/health/readiness
curl --fail http://127.0.0.1:8080/health
```

종료 시 `docker compose -f compose.demo.yml down`을 사용합니다. DB 데이터를 지우려는 경우에만
영향을 확인한 뒤 별도로 volume 삭제를 결정합니다.
멱등성 Smoke는 서버를 중지하되 volume을 유지하고 같은 설정으로 다시 기동합니다.

```bash
docker compose -f compose.demo.yml stop server
docker compose -f compose.demo.yml up --build server
```

두 번째 기동도 성공하고 응웬반A Worker
`92000000-0000-0000-0000-000000000006`가 한 건 유지되며, 응웬반A의 Golden Flow
Case·Task는 0건이어야 합니다. WorkerDocument는 Task·StoredFile 연결이 없는
`PASSPORT_COPY/VERIFIED` 1건과 `ARC/MISSING` 1건만 유지되어야 합니다. 다른 Showcase
Seed의 수량과 고정 ID도 첫 기동과 같아야 합니다.

종료 시 `docker compose -f compose.demo.yml down`을 사용합니다. DB 데이터를 지우려는
경우에만 정확한 Compose project와 전용 volume인지 확인한 뒤 별도로 volume 삭제를
결정합니다. 구버전 Golden Flow 예약 ID 감지로 기동이 중단된 개인 Demo DB만 초기화
대상이며, Seed가 기존 데이터를 자동 삭제하거나 Flyway로 정리하지 않습니다.

## 배포 후 Smoke

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
Expand All @@ -25,6 +26,13 @@
@Component
public class AiRuntimeContractValidator {

private static final Set<String> SERVER_OWNED_DOCUMENT_FIELDS = Set.of(
"passport_copy_status",
"passport_copy_expiry_date",
"arc_status",
"arc_expiry_date"
);

private static final long MIN_DEADLINE_MS = 100;
private static final long MAX_DEADLINE_MS = 60_000;
private static final int MAX_WORKERS = 1;
Expand Down Expand Up @@ -335,6 +343,16 @@ private void validateCoreValues(WorkerContext worker, AiCandidate candidate) {
"AI Runtime changed a Server-owned core value."
);
}
for (String fieldKey : SERVER_OWNED_DOCUMENT_FIELDS) {
String returnedValue = candidate.extractedSlots().get(fieldKey);
if (returnedValue != null
&& !Objects.equals(worker.requestedFields().get(fieldKey), returnedValue)) {
reject(
AiRuntimeFailureCode.CORE_VALUE_MISMATCH,
"AI Runtime changed a Server-owned document value."
);
}
}
}

private void validateAllowedSlot(String slot, Set<String> allowedSlots) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.fowoco.server.common.security.TenantDatabaseContext;
import com.fowoco.server.worker.application.WorkerAiContextSnapshot;
import com.fowoco.server.worker.application.port.WorkerAiContextReader;
import com.fowoco.server.worker.domain.DocumentType;
import com.fowoco.server.workflow.application.WorkflowCatalogService;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
Expand Down Expand Up @@ -144,10 +145,27 @@ private String resolveWorkerField(String fieldKey, WorkerAiContextSnapshot worke
case "worker_id" -> worker.workerId().toString();
case "stay_expiry_date" -> formatDate(worker.stayExpiryDate());
case "contract_end_date" -> formatDate(worker.contractEndDate());
case "passport_copy_status" -> documentStatus(worker, DocumentType.PASSPORT_COPY);
case "passport_copy_expiry_date" -> documentExpiryDate(
worker,
DocumentType.PASSPORT_COPY
);
case "arc_status" -> documentStatus(worker, DocumentType.ARC);
case "arc_expiry_date" -> documentExpiryDate(worker, DocumentType.ARC);
default -> null;
};
}

private String documentStatus(WorkerAiContextSnapshot worker, DocumentType documentType) {
var document = worker.documents().get(documentType);
return document == null ? null : document.submissionStatus().name();
}

private String documentExpiryDate(WorkerAiContextSnapshot worker, DocumentType documentType) {
var document = worker.documents().get(documentType);
return document == null ? null : formatDate(document.expiryDate());
}

private String formatDate(java.time.LocalDate date) {
return date == null ? null : date.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fowoco.server.demo.infrastructure.seed.DemoOperationalSeedCatalog.TaskSeed;
import com.fowoco.server.task.domain.TaskStatus;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Comparator;
Expand Down Expand Up @@ -67,8 +68,8 @@ INSERT INTO workflow_case (
DemoOperationalSeedCatalog.WORKFLOW_CATALOG_VERSION,
snapshot(tasks),
context.actorId(),
createdAt,
context.now()
Timestamp.from(createdAt),
Timestamp.from(context.now())
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ private static SubmissionStatus scenarioStatus(
) {
if (workerNumber == 6) {
return documentType == DocumentType.PASSPORT_COPY
? SubmissionStatus.MISSING
: SubmissionStatus.VERIFIED;
? SubmissionStatus.VERIFIED
: SubmissionStatus.MISSING;
}
return additionStatus(additionIndex);
}
Expand All @@ -155,8 +155,8 @@ private static Integer scenarioExpiryDays(
) {
if (workerNumber == 6) {
return switch (documentType) {
case PASSPORT_COPY -> null;
case ARC -> 365;
case PASSPORT_COPY -> 365;
case ARC -> null;
case CONTRACT -> 180;
case PERMIT -> throw new IllegalStateException("worker 6 has no permit document seed");
};
Expand All @@ -167,7 +167,7 @@ private static Integer scenarioExpiryDays(
private static String scenarioDestination(int workerNumber, DocumentType documentType) {
if (workerNumber == 6) {
return documentType == DocumentType.PASSPORT_COPY
? "근로자 문서 요청"
? "체류기간 연장"
: "재계약·연장 준비";
}
return destination(documentType);
Expand All @@ -180,8 +180,8 @@ private static String scenarioNote(
) {
if (workerNumber == 6) {
return switch (documentType) {
case PASSPORT_COPY -> "여권 사본 미보유 · 베트남어 요청 필요";
case ARC -> "외국인등록증 확인 완료";
case PASSPORT_COPY -> "검증된 유효 여권 사본";
case ARC -> "외국인등록증 사본 요청 필요";
case CONTRACT -> "현재 근로계약서 확인 완료";
case PERMIT -> throw new IllegalStateException("worker 6 has no permit document seed");
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.fowoco.server.demo.infrastructure.seed;

import java.util.List;
import java.util.Objects;
import java.util.UUID;
import org.springframework.jdbc.core.JdbcTemplate;

final class DemoGoldenFlowSeedStateGuard {

private static final String RESET_REQUIRED_MESSAGE =
"legacy Golden Flow demo seed rows detected; reset the personal demo database or "
+ "volume before starting with the Issue #94 seed contract";

private final JdbcTemplate jdbcTemplate;

DemoGoldenFlowSeedStateGuard(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = Objects.requireNonNull(jdbcTemplate, "jdbcTemplate must not be null");
}

void verifyNoLegacyRows(DemoOperationalSeedContext context) {
int legacyRowCount = count(
"workflow_case",
"case_id",
context.companyId(),
List.of(DemoOperationalSeedCatalog.RETIRED_GOLDEN_FLOW_CASE_ID)
) + count(
"task",
"task_id",
context.companyId(),
DemoOperationalSeedCatalog.RETIRED_GOLDEN_FLOW_TASK_IDS.stream().toList()
) + count(
"worker_document",
"worker_document_id",
context.companyId(),
DemoOperationalSeedCatalog.RETIRED_GOLDEN_FLOW_DOCUMENT_IDS.stream().toList()
) + count(
"audit_event",
"audit_event_id",
context.companyId(),
DemoOperationalSeedCatalog.RETIRED_GOLDEN_FLOW_AUDIT_IDS.stream().toList()
);
if (legacyRowCount > 0) {
throw new IllegalStateException(RESET_REQUIRED_MESSAGE);
}
}

private int count(String table, String idColumn, UUID companyId, List<UUID> ids) {
String placeholders = String.join(", ", java.util.Collections.nCopies(ids.size(), "?"));
Object[] parameters = new Object[ids.size() + 1];
parameters[0] = companyId;
for (int index = 0; index < ids.size(); index++) {
parameters[index + 1] = ids.get(index);
}
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM " + table + " WHERE company_id = ? AND "
+ idColumn + " IN (" + placeholders + ")",
Integer.class,
parameters
);
return count == null ? 0 : count;
}
}
Loading
Loading