refactor: migrate to clean architecture with trait-based DI (v0.2.0)

Complete architectural overhaul across all 12 crates:

- Replace validator crate with zod-rs for all DTO validation
- Replace manual pagination with paginator-rs/paginator-sea-orm
- Migrate all modules (iam, cms, gacha, dimentorin) to clean architecture:
  domain → application → infrastructure layers
- Introduce trait-based DI (Arc<dyn Trait>) at every layer for repositories and services
- Delete all v1/ legacy SurrealDB-era code across every crate
- Replace opaque response helpers with typed IntoResponse structs (ApiSuccess, ApiCreated, ApiPaginated, ApiMessage)
- Remove dual_mode_repository, migration_validation_errors, validator.rs dead code
- Zero cargo clippy warnings; release build clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-04-02 13:39:52 +07:00
co-authored by Claude Sonnet 4.6
parent 1b3366d735
commit e432a1a743
379 changed files with 9013 additions and 30532 deletions
@@ -0,0 +1,28 @@
use std::sync::Arc;
use async_trait::async_trait;
use uuid::Uuid;
use imphnen_utils::AppError;
use crate::gacha_claims::domain::{
GachaClaimDetail, GachaClaimEntity, GachaClaimRepository, GachaClaimService,
};
pub struct GachaClaimServiceImpl {
repo: Arc<dyn GachaClaimRepository>,
}
impl GachaClaimServiceImpl {
pub fn new(repo: Arc<dyn GachaClaimRepository>) -> Self {
Self { repo }
}
}
#[async_trait]
impl GachaClaimService for GachaClaimServiceImpl {
async fn get_claim(&self, id: Uuid) -> Result<GachaClaimDetail, AppError> {
self.repo.find_by_id(id).await
}
async fn create_claim(&self, entity: GachaClaimEntity) -> Result<(), AppError> {
self.repo.create(entity).await
}
}
@@ -0,0 +1,3 @@
pub mod gacha_claim_service;
pub use gacha_claim_service::GachaClaimServiceImpl;
@@ -0,0 +1,33 @@
use chrono::{DateTime, Utc};
use serde_json::Value;
use uuid::Uuid;
use imphnen_entities::UsersDetailQueryDto;
use crate::gacha_items::domain::gacha_item::GachaItemEntity;
#[derive(Clone, Debug)]
pub struct GachaClaimEntity {
pub id: Uuid,
pub user_id: Uuid,
pub gacha_item_id: Uuid,
pub claim_id: Uuid,
pub claim_type: String,
pub status: String,
pub quantity: i32,
pub metadata: Option<Value>,
pub is_deleted: bool,
pub claimed_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub deleted_at: Option<DateTime<Utc>>,
}
/// Denormalized struct for claim detail responses with nested user and item data.
#[derive(Clone, Debug)]
pub struct GachaClaimDetail {
pub id: Uuid,
pub user: UsersDetailQueryDto,
pub item: GachaItemEntity,
pub is_deleted: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
@@ -0,0 +1,7 @@
pub mod gacha_claim;
pub mod repository;
pub mod service;
pub use gacha_claim::{GachaClaimDetail, GachaClaimEntity};
pub use repository::GachaClaimRepository;
pub use service::GachaClaimService;
@@ -0,0 +1,10 @@
use async_trait::async_trait;
use uuid::Uuid;
use imphnen_utils::AppError;
use super::gacha_claim::{GachaClaimDetail, GachaClaimEntity};
#[async_trait]
pub trait GachaClaimRepository: Send + Sync {
async fn find_by_id(&self, id: Uuid) -> Result<GachaClaimDetail, AppError>;
async fn create(&self, entity: GachaClaimEntity) -> Result<(), AppError>;
}
@@ -0,0 +1,10 @@
use async_trait::async_trait;
use uuid::Uuid;
use imphnen_utils::AppError;
use super::gacha_claim::{GachaClaimDetail, GachaClaimEntity};
#[async_trait]
pub trait GachaClaimService: Send + Sync {
async fn get_claim(&self, id: Uuid) -> Result<GachaClaimDetail, AppError>;
async fn create_claim(&self, entity: GachaClaimEntity) -> Result<(), AppError>;
}
@@ -0,0 +1,41 @@
use imphnen_libs::ZodValidate;
use imphnen_iam::users::infrastructure::http::dto::UsersDetailItemDto;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::gacha_claims::domain::gacha_claim::GachaClaimDetail;
use crate::gacha_items::infrastructure::http::dto::GachaItemDto;
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct GachaClaimCreateRequestDto {
pub user_id: String,
pub item_id: String,
}
impl ZodValidate for GachaClaimCreateRequestDto {
fn zod_validate(value: &serde_json::Value) -> Result<Self, String> {
serde_json::from_value(value.clone()).map_err(|e| e.to_string())
}
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct GachaClaimDetailDto {
pub id: String,
pub user: UsersDetailItemDto,
pub item: GachaItemDto,
pub is_deleted: bool,
pub created_at: String,
pub updated_at: String,
}
impl From<GachaClaimDetail> for GachaClaimDetailDto {
fn from(detail: GachaClaimDetail) -> Self {
GachaClaimDetailDto {
id: detail.id.to_string(),
user: UsersDetailItemDto::from(&detail.user),
item: GachaItemDto::from(detail.item),
is_deleted: detail.is_deleted,
created_at: detail.created_at.to_rfc3339(),
updated_at: detail.updated_at.to_rfc3339(),
}
}
}
@@ -0,0 +1,77 @@
use std::sync::Arc;
use axum::{Extension, extract::Path, http::HeaderMap, response::IntoResponse};
use imphnen_libs::{AppState, ValidatedJson};
use imphnen_utils::{ApiSuccess, ApiMessage};
use imphnen_entities::ResponseSuccessDto;
use imphnen_iam::{PermissionsEnum, require_permissions};
use imphnen_utils::AppError;
use uuid::Uuid;
use super::dto::{GachaClaimCreateRequestDto, GachaClaimDetailDto};
use crate::gacha_claims::domain::{GachaClaimEntity, GachaClaimService};
#[utoipa::path(
get,
security(("Bearer" = [])),
path = "/v1/gacha/claims/detail/{id}",
params(
("id" = String, Path, description = "Gacha Claim ID")
),
responses(
(status = 200, description = "[ADMIN] Get gacha claim by ID", body = ResponseSuccessDto<GachaClaimDetailDto>)
),
tag = "Gacha"
)]
pub async fn get_gacha_claim_by_id(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Extension(service): Extension<Arc<dyn GachaClaimService>>,
Path(id): Path<String>,
) -> Result<impl IntoResponse, AppError> {
require_permissions!(headers, state, [PermissionsEnum::ReadDetailGachaClaims], {
let uuid = Uuid::parse_str(&id)
.map_err(|e| AppError::BadRequestError(format!("Invalid UUID: {e}")))?;
let detail = service.get_claim(uuid).await?;
Ok(ApiSuccess(GachaClaimDetailDto::from(detail)))
})
}
#[utoipa::path(
post,
security(("Bearer" = [])),
path = "/v1/gacha/claims/create",
request_body = GachaClaimCreateRequestDto,
responses(
(status = 201, description = "[ADMIN] Create new gacha claim")
),
tag = "Gacha"
)]
pub async fn post_create_gacha_claim(
headers: HeaderMap,
Extension(state): Extension<AppState>,
Extension(service): Extension<Arc<dyn GachaClaimService>>,
ValidatedJson(payload): ValidatedJson<GachaClaimCreateRequestDto>,
) -> Result<impl IntoResponse, AppError> {
require_permissions!(headers, state, [PermissionsEnum::CreateGachaClaims], {
let user_id = Uuid::parse_str(&payload.user_id)
.map_err(|e| AppError::BadRequestError(format!("Invalid user_id UUID: {e}")))?;
let item_id = Uuid::parse_str(&payload.item_id)
.map_err(|e| AppError::BadRequestError(format!("Invalid item_id UUID: {e}")))?;
let entity = GachaClaimEntity {
id: Uuid::new_v4(),
user_id,
gacha_item_id: item_id,
claim_id: Uuid::new_v4(),
claim_type: "standard".to_string(),
status: "claimed".to_string(),
quantity: 1,
metadata: None,
is_deleted: false,
claimed_at: chrono::Utc::now(),
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
deleted_at: None,
};
service.create_claim(entity).await?;
Ok(ApiMessage::created("Gacha claim created"))
})
}
@@ -0,0 +1,5 @@
pub mod dto;
pub mod handlers;
pub mod routes;
pub use routes::gacha_claim_router;
@@ -0,0 +1,20 @@
use std::sync::Arc;
use axum::{Router, routing::{get, post}, Extension};
use sea_orm::DatabaseConnection;
use crate::gacha_claims::application::GachaClaimServiceImpl;
use crate::gacha_claims::domain::GachaClaimService;
use crate::gacha_claims::infrastructure::persistence::PostgresGachaClaimRepository;
use super::handlers::{get_gacha_claim_by_id, post_create_gacha_claim};
fn build_service(db: DatabaseConnection, state: std::sync::Arc<imphnen_libs::AppState>) -> Arc<dyn GachaClaimService> {
let repo = Arc::new(PostgresGachaClaimRepository::new(db, state));
Arc::new(GachaClaimServiceImpl::new(repo))
}
pub fn gacha_claim_router(db: DatabaseConnection, state: std::sync::Arc<imphnen_libs::AppState>) -> Router {
let service = build_service(db, state);
Router::new()
.route("/detail/{id}", get(get_gacha_claim_by_id))
.route("/create", post(post_create_gacha_claim))
.layer(Extension(service))
}
@@ -0,0 +1,2 @@
pub mod http;
pub mod persistence;
@@ -0,0 +1,3 @@
pub mod postgres_gacha_claim_repository;
pub use postgres_gacha_claim_repository::PostgresGachaClaimRepository;
@@ -0,0 +1,105 @@
use std::sync::Arc;
use async_trait::async_trait;
use sea_orm::prelude::*;
use sea_orm::ActiveValue;
use uuid::Uuid;
use imphnen_utils::AppError;
use imphnen_entities::seaorm::gacha::gacha_claims::{
Entity as GachaClaimsEntity, ActiveModel as GachaClaimsActiveModel,
};
use imphnen_entities::seaorm::gacha::gacha_items::Entity as GachaItemsEntity;
use imphnen_libs::AppState;
use crate::gacha_claims::domain::{
gacha_claim::{GachaClaimDetail, GachaClaimEntity},
repository::GachaClaimRepository,
};
use crate::gacha_items::domain::gacha_item::GachaItemEntity;
pub struct PostgresGachaClaimRepository {
db: Arc<DatabaseConnection>,
state: Arc<AppState>,
}
impl PostgresGachaClaimRepository {
pub fn new(db: DatabaseConnection, state: Arc<AppState>) -> Self {
Self {
db: Arc::new(db),
state,
}
}
}
#[async_trait]
impl GachaClaimRepository for PostgresGachaClaimRepository {
async fn find_by_id(&self, id: Uuid) -> Result<GachaClaimDetail, AppError> {
let claim = GachaClaimsEntity::find_by_id(id)
.one(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?
.ok_or_else(|| AppError::NotFoundError("Gacha claim not found".to_string()))?;
let user = self.state.user_lookup_service
.get_user_by_id(claim.user_id, self.state.as_ref())
.await
.map(|info| info.basic_info)
.map_err(|e| AppError::InternalServerError(format!("Failed to fetch user: {e}")))?;
let item_model = GachaItemsEntity::find_by_id(claim.gacha_item_id)
.one(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?
.ok_or_else(|| AppError::NotFoundError("Gacha item not found".to_string()))?;
let item = GachaItemEntity {
id: item_model.id,
item_code: item_model.item_code,
name: item_model.name,
description: item_model.description,
rarity: item_model.rarity,
type_: item_model.type_,
category: item_model.category,
value: item_model.value,
weight: item_model.weight,
stock: item_model.stock,
is_limited: item_model.is_limited,
metadata: item_model.metadata,
is_deleted: item_model.deleted_at.is_some(),
created_at: item_model.created_at,
updated_at: item_model.updated_at,
deleted_at: item_model.deleted_at,
};
Ok(GachaClaimDetail {
id: claim.id,
user,
item,
is_deleted: claim.deleted_at.is_some(),
created_at: claim.created_at,
updated_at: claim.updated_at,
})
}
async fn create(&self, entity: GachaClaimEntity) -> Result<(), AppError> {
let active_model = GachaClaimsActiveModel {
id: ActiveValue::Set(entity.id),
user_id: ActiveValue::Set(entity.user_id),
gacha_item_id: ActiveValue::Set(entity.gacha_item_id),
claim_id: ActiveValue::Set(entity.claim_id),
claim_type: ActiveValue::Set(entity.claim_type),
status: ActiveValue::Set(entity.status),
quantity: ActiveValue::Set(entity.quantity),
metadata: ActiveValue::Set(entity.metadata),
created_at: ActiveValue::Set(entity.created_at),
updated_at: ActiveValue::Set(entity.updated_at),
deleted_at: ActiveValue::Set(entity.deleted_at),
claimed_at: ActiveValue::Set(entity.claimed_at),
};
GachaClaimsEntity::insert(active_model)
.exec(self.db.as_ref())
.await
.map_err(|e| AppError::InternalServerError(e.to_string()))?;
Ok(())
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod application;
pub mod domain;
pub mod infrastructure;
pub use infrastructure::http::gacha_claim_router;