feat(validation,rest-api,outbox): write-side end-to-end working

ordinis-validation:
- ValidationError, ValidationResult, ValidationException
- RecordValidator: networknt JSON Schema (Draft-7) + custom rule для
  x-localized полей. Walk schema, для каждого x-localized:true property
  проверяет: значение это object с locale keys (BCP 47 pattern xx-XX),
  все ключи в supported_locales, default_locale обязательно присутствует,
  значения non-empty strings.

ordinis-outbox:
- KafkaTopicResolver: ordinis.<bundle>.events.<scope>
- OutboxRecorder: запись события в outbox в той же транзакции (MANDATORY
  propagation), с tracer для trace_id/span_id из MDC если OTel доступен
- OutboxPoller: @Scheduled, batch_size + poll_interval из config, метрики
  ordinis_outbox_published_total / publish_errors_total / kafka_publish_duration_seconds
- OutboxLagGauge: ordinis_outbox_pending_events (gauge для алертов)
- ConditionalOnProperty ordinis.outbox.enabled — отключаем в read-api/projection-writer

ordinis-rest-api:
- DTOs: CreateDictionaryRequest, DictionaryResponse, CreateRecordRequest,
  RecordResponse (с WKT serialization geometry)
- DictionaryDefinitionService: CRUD definitions, outbox events DefinitionCreated/Updated
- DictionaryRecordService: bitemporal write-side, SERIALIZABLE isolation,
  bound check FAR_FUTURE valid_to. Update = close current + create new
  (никогда in-place). Outbox event RecordCreated/Updated/Deleted.
- DictionaryDefinitionController: GET list/by-name, POST create, PUT update
- DictionaryRecordController: GET active/history, POST create, PUT update,
  DELETE (close)
- OrdinisException + ApiErrorResponse + GlobalExceptionHandler
  (422 validation, 404 not found, 409 conflict, 500 internal)

ordinis-app:
- @EnableJpaRepositories + @EntityScan для multi-package сканирования
- application.yml profile-based: dev (PG localhost через port-forward,
  ddl-auto=update, OTel disabled), k8s (Cloud Config + Vault Kubernetes auth)
- spring.kafka producer config с SASL SCRAM-SHA-512

ordinis-domain:
- JpaAuditingConfig: добавлен DateTimeProvider возвращающий OffsetDateTime
  (Spring Data Auditing по дефолту его не поддерживает, без него падает
  IllegalArgumentException 'Cannot convert LocalDateTime to OffsetDateTime')
- @ConditionalOnBean(DataSource) убран — race с lifecycle ломал auditing

E2E test (port-forward к cluster PG+Kafka):
- POST dictionary 'ground_station' → 201, JSONB schema сохранена,
  supported_locales = {ru-RU, en-US}, default_locale = ru-RU
- POST record MOSCOW-1 multi-locale name + geometry POINT(37.618 55.751) → 201
- POST record с en-US без ru-RU → 422 с двумя ошибками:
  * networknt 'required property ru-RU not found'
  * custom 'x-localized.missing_default Default locale ru-RU is required'
- outbox_events: 2 row (DefinitionCreated + RecordCreated), topic
  ordinis.cuod.events.public, ключи правильные
- revinfo + dictionary_records_aud: 1 row (Envers tx-time history активен)
This commit is contained in:
Zimin A.N.
2026-05-03 13:04:36 +03:00
parent b778832f49
commit be77c30687
24 changed files with 1246 additions and 19 deletions
@@ -1,27 +1,28 @@
package cloud.nstart.terravault.ordinis.domain.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.auditing.DateTimeProvider;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import javax.sql.DataSource;
import java.time.OffsetDateTime;
import java.util.Optional;
/**
* Включает {@code @CreatedBy}/{@code @LastModifiedBy}. Domain не знает откуда
* приходит user identity — модули выше (app, rest-api) подключают свой
* {@link AuditorAware} (JWT subject из @nstart/auth). Default тут возвращает
* {@code "system"} для batch/internal операций.
* Включает {@code @CreatedBy}/{@code @LastModifiedBy}/{@code @CreatedDate}/{@code @LastModifiedDate}.
*
* <p>{@link ConditionalOnBean} на {@link DataSource} — без БД (dev smoke
* profile) auditing не активируется, JPA metamodel не строится.
* <p>Domain не знает откуда приходит user identity — модули выше (app,
* rest-api) подключают свой {@link AuditorAware} (JWT subject из @nstart/auth).
* Default тут возвращает {@code "system"} для batch/internal операций.
*
* <p>{@link DateTimeProvider} явно возвращает {@link OffsetDateTime} — Spring
* Data Auditing по дефолту не поддерживает {@code OffsetDateTime}, так что без
* этого bean'а получим {@code IllegalArgumentException} при сохранении.
*/
@Configuration
@ConditionalOnBean(DataSource.class)
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
@EnableJpaAuditing(auditorAwareRef = "auditorAware", dateTimeProviderRef = "auditingDateTimeProvider")
public class JpaAuditingConfig {
@Bean
@@ -29,4 +30,9 @@ public class JpaAuditingConfig {
public AuditorAware<String> auditorAware() {
return () -> Optional.of("system");
}
@Bean
public DateTimeProvider auditingDateTimeProvider() {
return () -> Optional.of(OffsetDateTime.now());
}
}