mirror of
https://github.com/RaisFast/raisfast.git
synced 2026-10-01 00:02:15 +00:00
refactor servers layer
This commit is contained in:
+5
-1
@@ -12,7 +12,7 @@ description = "A high-performance Headless CMS and API engine built with Rust +
|
||||
homepage = "https://github.com/snkzhong/raisfast"
|
||||
|
||||
[features]
|
||||
default = ["db-sqlite", "search-tantivy", "plugin-js", "plugin-lua", "plugin-rhai", "openapi"]
|
||||
default = ["db-sqlite", "search-tantivy", "plugin-js", "plugin-lua", "plugin-rhai", "openapi", "proxy"]
|
||||
|
||||
# ── Database backend (pick exactly one) ────────────────────────────
|
||||
db-sqlite = ["sqlx/sqlite"]
|
||||
@@ -202,3 +202,7 @@ required-features = ["export-types"]
|
||||
name = "tauri-app"
|
||||
path = "src/bin/tauri-app.rs"
|
||||
required-features = ["tauri"]
|
||||
|
||||
[[test]]
|
||||
name = "proxy_tests"
|
||||
required-features = ["proxy"]
|
||||
|
||||
+4
-528
@@ -15,11 +15,10 @@ use crate::eventbus::EventBus;
|
||||
use crate::plugins::PluginManager;
|
||||
|
||||
use super::{
|
||||
AccessCheckContext, AccessFilterContext, Advice, Aspect, ColumnDef, DataAfterCreateContext,
|
||||
DataAfterDeleteContext, DataAfterReadContext, DataAfterUpdateContext, DataBeforeCreateContext,
|
||||
DataBeforeDeleteContext, DataBeforeReadContext, DataBeforeUpdateContext, EventContext,
|
||||
HttpAfterContext, HttpBeforeContext, JoinPointId, Layer, Operation, Pointcut, TargetMatcher,
|
||||
When,
|
||||
Advice, Aspect, ColumnDef, DataAfterCreateContext, DataAfterDeleteContext,
|
||||
DataAfterReadContext, DataAfterUpdateContext, DataBeforeCreateContext, DataBeforeDeleteContext,
|
||||
DataBeforeReadContext, DataBeforeUpdateContext, HttpAfterContext, HttpBeforeContext,
|
||||
JoinPointId, Layer, Operation, Pointcut, TargetMatcher, When,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -119,7 +118,6 @@ impl AspectEngine {
|
||||
/// Returns `(filtered_data, Dispatched)` — caller gets both the typed result and aspect-modified record.
|
||||
pub async fn before_create<T: Clone + serde::Serialize + serde::de::DeserializeOwned + Send>(
|
||||
&self,
|
||||
_pool: &crate::db::Pool,
|
||||
table: &str,
|
||||
auth: &crate::middleware::auth::AuthUser,
|
||||
data: T,
|
||||
@@ -145,7 +143,6 @@ impl AspectEngine {
|
||||
/// Returns `(filtered_data, Dispatched)`.
|
||||
pub async fn before_update<T: Clone + serde::Serialize + serde::de::DeserializeOwned + Send>(
|
||||
&self,
|
||||
_pool: &crate::db::Pool,
|
||||
table: &str,
|
||||
auth: &crate::middleware::auth::AuthUser,
|
||||
old_data: &impl serde::Serialize,
|
||||
@@ -170,7 +167,6 @@ impl AspectEngine {
|
||||
/// Before-delete: aspect data dispatch.
|
||||
pub async fn before_delete(
|
||||
&self,
|
||||
_pool: &crate::db::Pool,
|
||||
table: &str,
|
||||
auth: &crate::middleware::auth::AuthUser,
|
||||
data: &impl serde::Serialize,
|
||||
@@ -490,52 +486,6 @@ impl AspectEngine {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Event Layer: pre-publish interception
|
||||
///
|
||||
/// Returns `Ok(true)` to continue publishing, `Ok(false)` if blocked by an aspect.
|
||||
pub async fn dispatch_event_before_publish(
|
||||
&self,
|
||||
event_type: &str,
|
||||
ctx: &mut EventContext,
|
||||
) -> Result<bool, anyhow::Error> {
|
||||
let jp_id = JoinPointId {
|
||||
layer: Layer::Event,
|
||||
operation: Operation::Publish,
|
||||
when: When::Before,
|
||||
};
|
||||
let aspects = self.get_aspects(&jp_id, event_type);
|
||||
for aspect in &aspects {
|
||||
match aspect.on_event_before_publish(ctx).await {
|
||||
Ok(Advice::Continue) => continue,
|
||||
Ok(Advice::Skip) => return Ok(false),
|
||||
Ok(Advice::Return(_)) => return Ok(false),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Event Layer: post-publish notification
|
||||
pub async fn dispatch_event_after_publish(
|
||||
&self,
|
||||
event_type: &str,
|
||||
ctx: &mut EventContext,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let jp_id = JoinPointId {
|
||||
layer: Layer::Event,
|
||||
operation: Operation::Publish,
|
||||
when: When::After,
|
||||
};
|
||||
let aspects = self.get_aspects(&jp_id, event_type);
|
||||
for aspect in &aspects {
|
||||
match aspect.on_event_after_publish(ctx).await {
|
||||
Ok(_) => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// HTTP Layer: pre-request interception
|
||||
///
|
||||
/// Returns `Ok(None)` to continue processing, `Ok(Some(body))` to short-circuit.
|
||||
@@ -581,52 +531,6 @@ impl AspectEngine {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Access Layer: permission check interception
|
||||
///
|
||||
/// Returns `Ok(true)` to allow, `Ok(false)` to deny.
|
||||
pub async fn dispatch_access_check(
|
||||
&self,
|
||||
subject: &str,
|
||||
ctx: &mut AccessCheckContext,
|
||||
) -> Result<bool, anyhow::Error> {
|
||||
let jp_id = JoinPointId {
|
||||
layer: Layer::Access,
|
||||
operation: Operation::Check,
|
||||
when: When::Before,
|
||||
};
|
||||
let aspects = self.get_aspects(&jp_id, subject);
|
||||
for aspect in &aspects {
|
||||
match aspect.on_access_check(ctx).await {
|
||||
Ok(Advice::Continue) => continue,
|
||||
Ok(Advice::Skip) => return Ok(false),
|
||||
Ok(Advice::Return(_)) => return Ok(false),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Access Layer: data filtering (query condition injection)
|
||||
pub async fn dispatch_access_filter(
|
||||
&self,
|
||||
table: &str,
|
||||
ctx: &mut AccessFilterContext,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let jp_id = JoinPointId {
|
||||
layer: Layer::Access,
|
||||
operation: Operation::Filter,
|
||||
when: When::Before,
|
||||
};
|
||||
let aspects = self.get_aspects(&jp_id, table);
|
||||
for aspect in &aspects {
|
||||
match aspect.on_access_filter(ctx).await {
|
||||
Ok(_) => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AspectEngine {
|
||||
@@ -1305,358 +1209,6 @@ mod tests {
|
||||
assert!(!engine.enable("nonexistent"));
|
||||
}
|
||||
|
||||
// ─── Event Layer tests ───
|
||||
|
||||
#[tokio::test]
|
||||
async fn event_before_publish_returns_true_when_no_aspects() {
|
||||
let engine = AspectEngine::new();
|
||||
let mut ctx = EventContext {
|
||||
base: BaseContext::new(None, "default".into(), "now".into()),
|
||||
event_type: "post_created".into(),
|
||||
payload: serde_json::json!({"id": "1"}),
|
||||
table: Some("posts".into()),
|
||||
};
|
||||
let result = engine
|
||||
.dispatch_event_before_publish("post_created", &mut ctx)
|
||||
.await;
|
||||
assert!(result.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn event_before_publish_continue_allows_publish() {
|
||||
struct LogEventAspect;
|
||||
#[async_trait::async_trait]
|
||||
impl Aspect for LogEventAspect {
|
||||
fn name(&self) -> &str {
|
||||
"log_event"
|
||||
}
|
||||
fn pointcuts(&self) -> Vec<Pointcut> {
|
||||
vec![Pointcut {
|
||||
layer: Layer::Event,
|
||||
operation: Operation::Publish,
|
||||
when: When::Before,
|
||||
target: TargetMatcher::All,
|
||||
}]
|
||||
}
|
||||
async fn on_event_before_publish(&self, ctx: &mut EventContext) -> AspectResult {
|
||||
ctx.payload
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("logged".into(), serde_json::Value::Bool(true));
|
||||
Ok(Advice::Continue)
|
||||
}
|
||||
}
|
||||
|
||||
let engine = AspectEngine::new();
|
||||
engine.register(LogEventAspect);
|
||||
|
||||
let mut ctx = EventContext {
|
||||
base: BaseContext::new(None, "default".into(), "now".into()),
|
||||
event_type: "post_created".into(),
|
||||
payload: serde_json::json!({"id": "1"}),
|
||||
table: Some("posts".into()),
|
||||
};
|
||||
let result = engine
|
||||
.dispatch_event_before_publish("post_created", &mut ctx)
|
||||
.await;
|
||||
assert!(result.unwrap());
|
||||
assert_eq!(ctx.payload["logged"], serde_json::Value::Bool(true));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn event_before_publish_skip_blocks_publish() {
|
||||
struct BlockAspect;
|
||||
#[async_trait::async_trait]
|
||||
impl Aspect for BlockAspect {
|
||||
fn name(&self) -> &str {
|
||||
"block_event"
|
||||
}
|
||||
fn pointcuts(&self) -> Vec<Pointcut> {
|
||||
vec![Pointcut {
|
||||
layer: Layer::Event,
|
||||
operation: Operation::Publish,
|
||||
when: When::Before,
|
||||
target: TargetMatcher::All,
|
||||
}]
|
||||
}
|
||||
async fn on_event_before_publish(&self, _ctx: &mut EventContext) -> AspectResult {
|
||||
Ok(Advice::Skip)
|
||||
}
|
||||
}
|
||||
|
||||
let engine = AspectEngine::new();
|
||||
engine.register(BlockAspect);
|
||||
|
||||
let mut ctx = EventContext {
|
||||
base: BaseContext::new(None, "default".into(), "now".into()),
|
||||
event_type: "post_created".into(),
|
||||
payload: serde_json::json!({}),
|
||||
table: None,
|
||||
};
|
||||
let result = engine
|
||||
.dispatch_event_before_publish("post_created", &mut ctx)
|
||||
.await;
|
||||
assert!(!result.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn event_after_publish_dispatches() {
|
||||
struct AfterPublishAspect;
|
||||
#[async_trait::async_trait]
|
||||
impl Aspect for AfterPublishAspect {
|
||||
fn name(&self) -> &str {
|
||||
"after_publish"
|
||||
}
|
||||
fn pointcuts(&self) -> Vec<Pointcut> {
|
||||
vec![Pointcut {
|
||||
layer: Layer::Event,
|
||||
operation: Operation::Publish,
|
||||
when: When::After,
|
||||
target: TargetMatcher::All,
|
||||
}]
|
||||
}
|
||||
async fn on_event_after_publish(&self, ctx: &mut EventContext) -> AspectResult {
|
||||
ctx.payload
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("after_ran".into(), serde_json::Value::Bool(true));
|
||||
Ok(Advice::Continue)
|
||||
}
|
||||
}
|
||||
|
||||
let engine = AspectEngine::new();
|
||||
engine.register(AfterPublishAspect);
|
||||
|
||||
let mut ctx = EventContext {
|
||||
base: BaseContext::new(None, "default".into(), "now".into()),
|
||||
event_type: "post_created".into(),
|
||||
payload: serde_json::json!({"id": "1"}),
|
||||
table: Some("posts".into()),
|
||||
};
|
||||
engine
|
||||
.dispatch_event_after_publish("post_created", &mut ctx)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(ctx.payload["after_ran"], serde_json::Value::Bool(true));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn event_target_filtering() {
|
||||
struct PostsOnlyEventAspect;
|
||||
#[async_trait::async_trait]
|
||||
impl Aspect for PostsOnlyEventAspect {
|
||||
fn name(&self) -> &str {
|
||||
"posts_event"
|
||||
}
|
||||
fn pointcuts(&self) -> Vec<Pointcut> {
|
||||
vec![Pointcut {
|
||||
layer: Layer::Event,
|
||||
operation: Operation::Publish,
|
||||
when: When::Before,
|
||||
target: TargetMatcher::Tables(vec!["post_created".into()]),
|
||||
}]
|
||||
}
|
||||
async fn on_event_before_publish(&self, ctx: &mut EventContext) -> AspectResult {
|
||||
ctx.payload
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert("posts_only".into(), serde_json::Value::Bool(true));
|
||||
Ok(Advice::Continue)
|
||||
}
|
||||
}
|
||||
|
||||
let engine = AspectEngine::new();
|
||||
engine.register(PostsOnlyEventAspect);
|
||||
|
||||
let mut ctx = EventContext {
|
||||
base: BaseContext::new(None, "default".into(), "now".into()),
|
||||
event_type: "comment_created".into(),
|
||||
payload: serde_json::json!({}),
|
||||
table: Some("comments".into()),
|
||||
};
|
||||
engine
|
||||
.dispatch_event_before_publish("comment_created", &mut ctx)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!ctx.payload.as_object().unwrap().contains_key("posts_only"));
|
||||
}
|
||||
|
||||
// ─── Access Layer tests ───
|
||||
|
||||
#[tokio::test]
|
||||
async fn access_check_allows_when_no_aspects() {
|
||||
let engine = AspectEngine::new();
|
||||
let mut ctx = AccessCheckContext {
|
||||
base: BaseContext::new(Some("u1".into()), "default".into(), "now".into()),
|
||||
route: "/api/v1/admin/posts".into(),
|
||||
method: "PUT".into(),
|
||||
table: Some("posts".into()),
|
||||
action: "update".into(),
|
||||
};
|
||||
let result = engine
|
||||
.dispatch_access_check("posts", &mut ctx)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn access_check_skip_denies_access() {
|
||||
struct DenyAspect;
|
||||
#[async_trait::async_trait]
|
||||
impl Aspect for DenyAspect {
|
||||
fn name(&self) -> &str {
|
||||
"deny_all"
|
||||
}
|
||||
fn pointcuts(&self) -> Vec<Pointcut> {
|
||||
vec![Pointcut {
|
||||
layer: Layer::Access,
|
||||
operation: Operation::Check,
|
||||
when: When::Before,
|
||||
target: TargetMatcher::All,
|
||||
}]
|
||||
}
|
||||
async fn on_access_check(&self, _ctx: &mut AccessCheckContext) -> AspectResult {
|
||||
Ok(Advice::Skip)
|
||||
}
|
||||
}
|
||||
|
||||
let engine = AspectEngine::new();
|
||||
engine.register(DenyAspect);
|
||||
|
||||
let mut ctx = AccessCheckContext {
|
||||
base: BaseContext::new(Some("u1".into()), "default".into(), "now".into()),
|
||||
route: "/api/v1/admin/posts/123".into(),
|
||||
method: "DELETE".into(),
|
||||
table: Some("posts".into()),
|
||||
action: "delete".into(),
|
||||
};
|
||||
let result = engine
|
||||
.dispatch_access_check("posts", &mut ctx)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn access_check_continue_allows() {
|
||||
struct AllowAspect;
|
||||
#[async_trait::async_trait]
|
||||
impl Aspect for AllowAspect {
|
||||
fn name(&self) -> &str {
|
||||
"allow"
|
||||
}
|
||||
fn pointcuts(&self) -> Vec<Pointcut> {
|
||||
vec![Pointcut {
|
||||
layer: Layer::Access,
|
||||
operation: Operation::Check,
|
||||
when: When::Before,
|
||||
target: TargetMatcher::All,
|
||||
}]
|
||||
}
|
||||
async fn on_access_check(&self, _ctx: &mut AccessCheckContext) -> AspectResult {
|
||||
Ok(Advice::Continue)
|
||||
}
|
||||
}
|
||||
|
||||
let engine = AspectEngine::new();
|
||||
engine.register(AllowAspect);
|
||||
|
||||
let mut ctx = AccessCheckContext {
|
||||
base: BaseContext::new(Some("u1".into()), "default".into(), "now".into()),
|
||||
route: "/api/v1/posts".into(),
|
||||
method: "GET".into(),
|
||||
table: Some("posts".into()),
|
||||
action: "read".into(),
|
||||
};
|
||||
let result = engine
|
||||
.dispatch_access_check("posts", &mut ctx)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn access_filter_adds_conditions() {
|
||||
struct TenantFilterAspect;
|
||||
#[async_trait::async_trait]
|
||||
impl Aspect for TenantFilterAspect {
|
||||
fn name(&self) -> &str {
|
||||
"tenant_filter"
|
||||
}
|
||||
fn pointcuts(&self) -> Vec<Pointcut> {
|
||||
vec![Pointcut {
|
||||
layer: Layer::Access,
|
||||
operation: Operation::Filter,
|
||||
when: When::Before,
|
||||
target: TargetMatcher::All,
|
||||
}]
|
||||
}
|
||||
async fn on_access_filter(&self, ctx: &mut AccessFilterContext) -> AspectResult {
|
||||
ctx.conditions.push("tenant_id = ?".into());
|
||||
ctx.params.push(ctx.base.tenant_id.clone());
|
||||
Ok(Advice::Continue)
|
||||
}
|
||||
}
|
||||
|
||||
let engine = AspectEngine::new();
|
||||
engine.register(TenantFilterAspect);
|
||||
|
||||
let mut ctx = AccessFilterContext {
|
||||
base: BaseContext::new(None, "t1".into(), "now".into()),
|
||||
table: "posts".into(),
|
||||
conditions: vec![],
|
||||
params: vec![],
|
||||
};
|
||||
engine
|
||||
.dispatch_access_filter("posts", &mut ctx)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(ctx.conditions.contains(&"tenant_id = ?".to_string()));
|
||||
assert!(ctx.params.contains(&"t1".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn access_check_error_aborts() {
|
||||
struct FailCheckAspect;
|
||||
#[async_trait::async_trait]
|
||||
impl Aspect for FailCheckAspect {
|
||||
fn name(&self) -> &str {
|
||||
"fail_check"
|
||||
}
|
||||
fn pointcuts(&self) -> Vec<Pointcut> {
|
||||
vec![Pointcut {
|
||||
layer: Layer::Access,
|
||||
operation: Operation::Check,
|
||||
when: When::Before,
|
||||
target: TargetMatcher::All,
|
||||
}]
|
||||
}
|
||||
async fn on_access_check(&self, _ctx: &mut AccessCheckContext) -> AspectResult {
|
||||
Err(anyhow::anyhow!("rbac lookup failed"))
|
||||
}
|
||||
}
|
||||
|
||||
let engine = AspectEngine::new();
|
||||
engine.register(FailCheckAspect);
|
||||
|
||||
let mut ctx = AccessCheckContext {
|
||||
base: BaseContext::new(None, "default".into(), "now".into()),
|
||||
route: "/api/v1/posts".into(),
|
||||
method: "GET".into(),
|
||||
table: Some("posts".into()),
|
||||
action: "read".into(),
|
||||
};
|
||||
let result = engine.dispatch_access_check("posts", &mut ctx).await;
|
||||
assert!(result.is_err());
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("rbac lookup failed")
|
||||
);
|
||||
}
|
||||
|
||||
// ─── HTTP Layer tests ───
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1787,80 +1339,4 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(ctx.response_body.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn event_priority_order() {
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let log: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
struct LateAspect {
|
||||
log: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl Aspect for LateAspect {
|
||||
fn name(&self) -> &str {
|
||||
"late"
|
||||
}
|
||||
fn priority(&self) -> i32 {
|
||||
100
|
||||
}
|
||||
fn pointcuts(&self) -> Vec<Pointcut> {
|
||||
vec![Pointcut {
|
||||
layer: Layer::Event,
|
||||
operation: Operation::Publish,
|
||||
when: When::Before,
|
||||
target: TargetMatcher::All,
|
||||
}]
|
||||
}
|
||||
async fn on_event_before_publish(&self, _ctx: &mut EventContext) -> AspectResult {
|
||||
self.log.lock().unwrap().push("late".into());
|
||||
Ok(Advice::Continue)
|
||||
}
|
||||
}
|
||||
|
||||
struct EarlyAspect {
|
||||
log: Arc<Mutex<Vec<String>>>,
|
||||
}
|
||||
#[async_trait::async_trait]
|
||||
impl Aspect for EarlyAspect {
|
||||
fn name(&self) -> &str {
|
||||
"early"
|
||||
}
|
||||
fn priority(&self) -> i32 {
|
||||
-100
|
||||
}
|
||||
fn pointcuts(&self) -> Vec<Pointcut> {
|
||||
vec![Pointcut {
|
||||
layer: Layer::Event,
|
||||
operation: Operation::Publish,
|
||||
when: When::Before,
|
||||
target: TargetMatcher::All,
|
||||
}]
|
||||
}
|
||||
async fn on_event_before_publish(&self, _ctx: &mut EventContext) -> AspectResult {
|
||||
self.log.lock().unwrap().push("early".into());
|
||||
Ok(Advice::Continue)
|
||||
}
|
||||
}
|
||||
|
||||
let engine = AspectEngine::new();
|
||||
engine.register(LateAspect { log: log.clone() });
|
||||
engine.register(EarlyAspect { log: log.clone() });
|
||||
|
||||
let mut ctx = EventContext {
|
||||
base: BaseContext::new(None, "default".into(), "now".into()),
|
||||
event_type: "test".into(),
|
||||
payload: serde_json::json!({}),
|
||||
table: None,
|
||||
};
|
||||
engine
|
||||
.dispatch_event_before_publish("test", &mut ctx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let entries = log.lock().unwrap();
|
||||
assert_eq!(entries[0], "early");
|
||||
assert_eq!(entries[1], "late");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
//! Page-related commands
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::models::page::PageStatus;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreatePageCmd {
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
@@ -19,6 +22,7 @@ pub struct CreatePageCmd {
|
||||
pub cover_image: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdatePageCmd {
|
||||
pub id: i64,
|
||||
pub title: Option<String>,
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "export-types")]
|
||||
use ts_rs::TS;
|
||||
use utoipa::ToSchema;
|
||||
@@ -7,7 +7,7 @@ use validator::Validate;
|
||||
use super::validate_optional_uuid;
|
||||
|
||||
#[cfg_attr(feature = "export-types", derive(TS))]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateCategoryRequest {
|
||||
#[validate(length(min = 1, max = 100))]
|
||||
pub name: String,
|
||||
@@ -18,7 +18,7 @@ pub struct CreateCategoryRequest {
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "export-types", derive(TS))]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateCategoryRequest {
|
||||
#[validate(length(min = 1, max = 100))]
|
||||
pub name: Option<String>,
|
||||
|
||||
+4
-4
@@ -7,7 +7,7 @@ use validator::Validate;
|
||||
use super::validate_optional_uuid;
|
||||
|
||||
#[cfg_attr(feature = "export-types", derive(TS))]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateProductRequest {
|
||||
#[validate(length(min = 1, max = 200))]
|
||||
pub title: String,
|
||||
@@ -39,7 +39,7 @@ pub struct CreateProductRequest {
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "export-types", derive(TS))]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct UpdateProductRequest {
|
||||
#[validate(length(min = 1, max = 200))]
|
||||
pub title: Option<String>,
|
||||
@@ -73,7 +73,7 @@ pub struct UpdateProductRequest {
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "export-types", derive(TS))]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Validate, ToSchema)]
|
||||
pub struct CreateOrderRequest {
|
||||
#[validate(length(min = 1))]
|
||||
pub items: Vec<CreateOrderItemRequest>,
|
||||
@@ -86,7 +86,7 @@ pub struct CreateOrderRequest {
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "export-types", derive(TS))]
|
||||
#[derive(Debug, Serialize, Deserialize, Validate, ToSchema)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Validate, ToSchema)]
|
||||
pub struct CreateOrderItemRequest {
|
||||
pub product_id: String,
|
||||
#[validate(range(min = 1))]
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(feature = "export-types")]
|
||||
use ts_rs::TS;
|
||||
use utoipa::ToSchema;
|
||||
use validator::Validate;
|
||||
|
||||
#[cfg_attr(feature = "export-types", derive(TS))]
|
||||
#[derive(Debug, Deserialize, Validate, ToSchema)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Validate, ToSchema)]
|
||||
pub struct CreateTagRequest {
|
||||
#[validate(length(min = 1, max = 50))]
|
||||
pub name: String,
|
||||
|
||||
@@ -10,12 +10,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::dto::PostResponse;
|
||||
use crate::models::category::Category;
|
||||
use crate::models::comment::Comment;
|
||||
use crate::models::email_verification::EmailVerificationToken;
|
||||
use crate::models::media::Media;
|
||||
use crate::models::order::Order;
|
||||
use crate::models::page::Page;
|
||||
use crate::models::password_reset::PasswordResetToken;
|
||||
use crate::models::payment_order::PaymentOrder;
|
||||
use crate::models::post::Post;
|
||||
use crate::models::product::Product;
|
||||
use crate::models::tag::Tag;
|
||||
use crate::models::user::User;
|
||||
use crate::models::wallet_transaction::WalletTransaction;
|
||||
|
||||
pub use raisfast_derive::EventMeta;
|
||||
|
||||
@@ -40,6 +47,68 @@ pub enum Event {
|
||||
CommentCreating,
|
||||
#[event(table = "comments")]
|
||||
CommentCreated(Comment),
|
||||
#[event(table = "comments")]
|
||||
CommentUpdated(Comment),
|
||||
#[event(table = "comments")]
|
||||
CommentDeleted(Comment),
|
||||
|
||||
// ── Tag lifecycle ──
|
||||
#[event(table = "tags")]
|
||||
TagCreated(Tag),
|
||||
#[event(table = "tags")]
|
||||
TagUpdated(Tag),
|
||||
#[event(table = "tags")]
|
||||
TagDeleted(Tag),
|
||||
|
||||
// ── Category lifecycle ──
|
||||
#[event(table = "categories")]
|
||||
CategoryCreated(Category),
|
||||
#[event(table = "categories")]
|
||||
CategoryUpdated(Category),
|
||||
#[event(table = "categories")]
|
||||
CategoryDeleted(Category),
|
||||
|
||||
// ── Page lifecycle ──
|
||||
#[event(table = "pages")]
|
||||
PageCreated(Page),
|
||||
#[event(table = "pages")]
|
||||
PageUpdated(Page),
|
||||
#[event(table = "pages")]
|
||||
PageDeleted(Page),
|
||||
|
||||
// ── Product lifecycle ──
|
||||
#[event(table = "products")]
|
||||
ProductCreated(Product),
|
||||
#[event(table = "products")]
|
||||
ProductUpdated(Product),
|
||||
#[event(table = "products")]
|
||||
ProductDeleted(Product),
|
||||
|
||||
// ── Order lifecycle ──
|
||||
#[event(table = "orders")]
|
||||
OrderCreated(Order),
|
||||
#[event(table = "orders")]
|
||||
OrderPaid(Order),
|
||||
#[event(table = "orders")]
|
||||
OrderShipped(Order),
|
||||
#[event(table = "orders")]
|
||||
OrderCompleted(Order),
|
||||
#[event(table = "orders")]
|
||||
OrderCancelled(Order),
|
||||
|
||||
// ── Payment lifecycle ──
|
||||
#[event(table = "payment_orders")]
|
||||
PaymentOrderCreated(PaymentOrder),
|
||||
#[event(table = "payment_orders")]
|
||||
PaymentPaid(PaymentOrder),
|
||||
#[event(table = "payment_orders")]
|
||||
PaymentRefunded(PaymentOrder),
|
||||
|
||||
// ── Wallet lifecycle ──
|
||||
#[event(table = "wallet_transactions")]
|
||||
WalletCredited(WalletTransaction),
|
||||
#[event(table = "wallet_transactions")]
|
||||
WalletDebited(WalletTransaction),
|
||||
|
||||
// ── Generic CMS content lifecycle ──
|
||||
ContentCreating,
|
||||
@@ -65,10 +134,12 @@ pub enum Event {
|
||||
MediaDeleted(Media),
|
||||
|
||||
// ── Auth ──
|
||||
#[event(table = "users")]
|
||||
PasswordResetRequested {
|
||||
user: User,
|
||||
token: PasswordResetToken,
|
||||
},
|
||||
#[event(table = "users")]
|
||||
EmailVerificationRequested {
|
||||
user_id: i64,
|
||||
email: String,
|
||||
|
||||
@@ -197,7 +197,7 @@ pub async fn register(
|
||||
validation::validate(&req)?;
|
||||
let user = auth::register(
|
||||
state.user_repo.as_ref(),
|
||||
&state.eventbus,
|
||||
&state.aspect_engine,
|
||||
req,
|
||||
auth.tenant_id(),
|
||||
state.config.require_email_verification,
|
||||
@@ -221,8 +221,7 @@ pub async fn login(
|
||||
let resp = auth::login(
|
||||
state.user_repo.as_ref(),
|
||||
state.refresh_token_repo.as_ref(),
|
||||
&state.plugins,
|
||||
&state.eventbus,
|
||||
&state.aspect_engine,
|
||||
&state.pool,
|
||||
&req,
|
||||
&state.config.jwt_secret,
|
||||
@@ -254,7 +253,7 @@ pub async fn resend_verification(
|
||||
email_verification::resend_verification(
|
||||
&state.pool,
|
||||
state.user_repo.as_ref(),
|
||||
&state.eventbus,
|
||||
&state.aspect_engine,
|
||||
&req.email,
|
||||
)
|
||||
.await?;
|
||||
@@ -312,7 +311,7 @@ pub async fn forgot_password(
|
||||
password_reset::forgot_password(
|
||||
&state.pool,
|
||||
state.user_repo.as_ref(),
|
||||
&state.eventbus,
|
||||
&state.aspect_engine,
|
||||
&req.email,
|
||||
auth.tenant_id(),
|
||||
)
|
||||
|
||||
+16
-26
@@ -8,7 +8,6 @@ use crate::errors::app_error::AppResult;
|
||||
use crate::errors::response::{ApiResponse, PaginatedData};
|
||||
use crate::errors::validation;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::services::category;
|
||||
use crate::utils::pagination::PaginationParams;
|
||||
|
||||
pub fn routes(
|
||||
@@ -130,13 +129,10 @@ pub async fn list(
|
||||
) -> AppResult<ApiResponse<crate::errors::response::PaginatedData<crate::models::category::Category>>>
|
||||
{
|
||||
params.sanitize();
|
||||
let (items, total) = category::list_categories_paginated(
|
||||
state.category_repo.as_ref(),
|
||||
&auth,
|
||||
params.page,
|
||||
params.page_size,
|
||||
)
|
||||
.await?;
|
||||
let (items, total) = state
|
||||
.category_service
|
||||
.list_paginated(&auth, params.page, params.page_size)
|
||||
.await?;
|
||||
Ok(params.paginate(items, total))
|
||||
}
|
||||
|
||||
@@ -150,7 +146,7 @@ pub async fn get(
|
||||
State(state): State<crate::AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<crate::models::category::Category>> {
|
||||
let cat = category::get_category(state.category_repo.as_ref(), &id, &auth).await?;
|
||||
let cat = state.category_service.get(&id, &auth).await?;
|
||||
Ok(ApiResponse::success(cat))
|
||||
}
|
||||
|
||||
@@ -167,7 +163,7 @@ pub async fn create(
|
||||
) -> AppResult<ApiResponse<crate::models::category::Category>> {
|
||||
auth.ensure_author()?;
|
||||
validation::validate(&req)?;
|
||||
let cat = category::create_category(state.category_repo.as_ref(), &auth, req).await?;
|
||||
let cat = state.category_service.create(&auth, req).await?;
|
||||
Ok(ApiResponse::success(cat))
|
||||
}
|
||||
|
||||
@@ -186,7 +182,7 @@ pub async fn update(
|
||||
) -> AppResult<ApiResponse<crate::models::category::Category>> {
|
||||
auth.ensure_author()?;
|
||||
validation::validate(&req)?;
|
||||
let cat = category::update_category(state.category_repo.as_ref(), &auth, &id, req).await?;
|
||||
let cat = state.category_service.update(&auth, &id, req).await?;
|
||||
Ok(ApiResponse::success(cat))
|
||||
}
|
||||
|
||||
@@ -202,7 +198,7 @@ pub async fn delete(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_author()?;
|
||||
category::delete_category(state.category_repo.as_ref(), &id, &auth).await?;
|
||||
state.category_service.delete(&id, &auth).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -215,13 +211,10 @@ pub async fn admin_list(
|
||||
) -> AppResult<ApiResponse<PaginatedData<crate::models::category::Category>>> {
|
||||
auth.ensure_admin()?;
|
||||
params.sanitize();
|
||||
let (items, total) = category::list_categories_paginated(
|
||||
state.category_repo.as_ref(),
|
||||
&auth,
|
||||
params.page,
|
||||
params.page_size,
|
||||
)
|
||||
.await?;
|
||||
let (items, total) = state
|
||||
.category_service
|
||||
.list_paginated(&auth, params.page, params.page_size)
|
||||
.await?;
|
||||
Ok(params.paginate(items, total))
|
||||
}
|
||||
|
||||
@@ -232,7 +225,7 @@ pub async fn admin_create(
|
||||
) -> AppResult<ApiResponse<crate::models::category::Category>> {
|
||||
auth.ensure_admin()?;
|
||||
validation::validate(&req)?;
|
||||
let cat = category::create_category(state.category_repo.as_ref(), &auth, req).await?;
|
||||
let cat = state.category_service.create(&auth, req).await?;
|
||||
Ok(ApiResponse::success(cat))
|
||||
}
|
||||
|
||||
@@ -244,7 +237,7 @@ pub async fn admin_update(
|
||||
) -> AppResult<ApiResponse<crate::models::category::Category>> {
|
||||
auth.ensure_admin()?;
|
||||
validation::validate(&req)?;
|
||||
let cat = category::update_category(state.category_repo.as_ref(), &auth, &id, req).await?;
|
||||
let cat = state.category_service.update(&auth, &id, req).await?;
|
||||
Ok(ApiResponse::success(cat))
|
||||
}
|
||||
|
||||
@@ -254,7 +247,7 @@ pub async fn admin_delete(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_admin()?;
|
||||
category::delete_category(state.category_repo.as_ref(), &id, &auth).await?;
|
||||
state.category_service.delete(&id, &auth).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -268,10 +261,7 @@ pub async fn admin_batch(
|
||||
let mut affected = 0usize;
|
||||
if req.action == "delete" {
|
||||
for id in &req.ids {
|
||||
if category::delete_category(state.category_repo.as_ref(), id, &auth)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
if state.category_service.delete(id, &auth).await.is_ok() {
|
||||
affected += 1;
|
||||
}
|
||||
}
|
||||
|
||||
+40
-52
@@ -15,7 +15,6 @@ use crate::errors::response::{ApiResponse, PaginatedData};
|
||||
use crate::errors::validation;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::models::comment::CommentStatus;
|
||||
use crate::services::comment as comment_service;
|
||||
use crate::utils::pagination::PaginationParams;
|
||||
|
||||
pub fn routes(
|
||||
@@ -152,15 +151,10 @@ pub async fn list(
|
||||
> {
|
||||
let mut p = params;
|
||||
p.sanitize();
|
||||
let (comments, total) = comment_service::list_comments_paginated(
|
||||
state.post_repo.as_ref(),
|
||||
state.comment_repo.as_ref(),
|
||||
&slug,
|
||||
p.page,
|
||||
p.page_size,
|
||||
&auth,
|
||||
)
|
||||
.await?;
|
||||
let (comments, total) = state
|
||||
.comment_service
|
||||
.list_paginated(&slug, p.page, p.page_size, &auth)
|
||||
.await?;
|
||||
Ok(p.paginate(comments, total))
|
||||
}
|
||||
|
||||
@@ -199,19 +193,17 @@ pub async fn create(
|
||||
) -> AppResult<ApiResponse<crate::models::comment::CommentResponse>> {
|
||||
validation::validate(&req)?;
|
||||
|
||||
let comment = comment_service::create_comment(
|
||||
state.post_repo.as_ref(),
|
||||
state.comment_repo.as_ref(),
|
||||
&state.plugins,
|
||||
&state.eventbus,
|
||||
&slug,
|
||||
&auth,
|
||||
&req.content,
|
||||
req.parent_id.as_deref(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let comment = state
|
||||
.comment_service
|
||||
.create(
|
||||
&slug,
|
||||
&auth,
|
||||
&req.content,
|
||||
req.parent_id.as_deref(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(ApiResponse::success(comment))
|
||||
}
|
||||
@@ -235,19 +227,17 @@ pub async fn create_guest(
|
||||
.as_deref()
|
||||
.ok_or_else(|| AppError::BadRequest("nickname_required".into()))?;
|
||||
|
||||
let comment = comment_service::create_comment(
|
||||
state.post_repo.as_ref(),
|
||||
state.comment_repo.as_ref(),
|
||||
&state.plugins,
|
||||
&state.eventbus,
|
||||
&slug,
|
||||
&auth,
|
||||
&req.content,
|
||||
req.parent_id.as_deref(),
|
||||
Some(nickname),
|
||||
req.email.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let comment = state
|
||||
.comment_service
|
||||
.create(
|
||||
&slug,
|
||||
&auth,
|
||||
&req.content,
|
||||
req.parent_id.as_deref(),
|
||||
Some(nickname),
|
||||
req.email.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(ApiResponse::success(comment))
|
||||
}
|
||||
@@ -263,7 +253,7 @@ pub async fn delete(
|
||||
State(state): State<crate::AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
comment_service::delete_comment(state.comment_repo.as_ref(), &id, &auth).await?;
|
||||
state.comment_service.delete(&id, &auth).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -282,7 +272,9 @@ pub async fn update_status(
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_admin()?;
|
||||
validation::validate(&req)?;
|
||||
comment_service::update_comment_status(state.comment_repo.as_ref(), &id, req.status, &auth)
|
||||
state
|
||||
.comment_service
|
||||
.update_status(&id, req.status, &auth)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
@@ -321,7 +313,9 @@ pub async fn admin_update_status(
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_admin()?;
|
||||
validation::validate(&req)?;
|
||||
comment_service::update_comment_status(state.comment_repo.as_ref(), &id, req.status, &auth)
|
||||
state
|
||||
.comment_service
|
||||
.update_status(&id, req.status, &auth)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
@@ -337,7 +331,7 @@ pub async fn admin_delete(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_admin()?;
|
||||
comment_service::delete_comment(state.comment_repo.as_ref(), &id, &auth).await?;
|
||||
state.comment_service.delete(&id, &auth).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -357,10 +351,7 @@ pub async fn admin_batch(
|
||||
for id in &req.ids {
|
||||
match req.action.as_str() {
|
||||
"delete" => {
|
||||
if comment_service::delete_comment(state.comment_repo.as_ref(), id, &auth)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
if state.comment_service.delete(id, &auth).await.is_ok() {
|
||||
affected += 1;
|
||||
}
|
||||
}
|
||||
@@ -371,14 +362,11 @@ pub async fn admin_batch(
|
||||
"spam" => CommentStatus::Spam,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if comment_service::update_comment_status(
|
||||
state.comment_repo.as_ref(),
|
||||
id,
|
||||
status,
|
||||
&auth,
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
if state
|
||||
.comment_service
|
||||
.update_status(id, status, &auth)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
affected += 1;
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ pub async fn callback(
|
||||
&state.config.jwt_secret,
|
||||
state.config.jwt_access_expires,
|
||||
state.config.jwt_refresh_expires,
|
||||
&state.eventbus,
|
||||
&state.aspect_engine,
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
+25
-50
@@ -9,7 +9,6 @@ use crate::errors::app_error::AppResult;
|
||||
use crate::errors::response::ApiResponse;
|
||||
use crate::errors::validation;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::services::order;
|
||||
use crate::utils::pagination::PaginationParams;
|
||||
|
||||
pub fn routes(
|
||||
@@ -197,15 +196,7 @@ pub async fn create_order(
|
||||
.user_int_id()
|
||||
.ok_or(crate::errors::app_error::AppError::Unauthorized)?;
|
||||
validation::validate(&req)?;
|
||||
let o = order::create_order(
|
||||
&state.pool,
|
||||
state.product_repo.as_ref(),
|
||||
state.order_repo.as_ref(),
|
||||
&auth,
|
||||
user_int_id,
|
||||
req,
|
||||
)
|
||||
.await?;
|
||||
let o = state.order_service.create(&auth, user_int_id, req).await?;
|
||||
let items = state
|
||||
.order_repo
|
||||
.find_items_by_order_id(o.id, auth.tenant_id())
|
||||
@@ -227,14 +218,10 @@ pub async fn list_orders(
|
||||
.user_int_id()
|
||||
.ok_or(crate::errors::app_error::AppError::Unauthorized)?;
|
||||
params.sanitize();
|
||||
let (orders, total) = order::list_user_orders(
|
||||
state.order_repo.as_ref(),
|
||||
&auth,
|
||||
user_int_id,
|
||||
params.page,
|
||||
params.page_size,
|
||||
)
|
||||
.await?;
|
||||
let (orders, total) = state
|
||||
.order_service
|
||||
.list_user(&auth, user_int_id, params.page, params.page_size)
|
||||
.await?;
|
||||
let mut responses = Vec::new();
|
||||
for o in orders {
|
||||
let items = state
|
||||
@@ -257,7 +244,7 @@ pub async fn get_order(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<OrderResponse>> {
|
||||
auth.ensure_authenticated()?;
|
||||
let (o, items) = order::get_order(state.order_repo.as_ref(), &auth, &id).await?;
|
||||
let (o, items) = state.order_service.get(&auth, &id).await?;
|
||||
Ok(ApiResponse::success(to_order_response(o, items)))
|
||||
}
|
||||
|
||||
@@ -277,14 +264,7 @@ pub async fn cancel_order_handler(
|
||||
let user_int_id = auth
|
||||
.user_int_id()
|
||||
.ok_or(crate::errors::app_error::AppError::Unauthorized)?;
|
||||
order::cancel_order(
|
||||
&state.pool,
|
||||
state.order_repo.as_ref(),
|
||||
&auth,
|
||||
&id,
|
||||
user_int_id,
|
||||
)
|
||||
.await?;
|
||||
state.order_service.cancel(&auth, &id, user_int_id).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -302,14 +282,10 @@ pub async fn confirm_receipt(
|
||||
let user_int_id = auth
|
||||
.user_int_id()
|
||||
.ok_or(crate::errors::app_error::AppError::Unauthorized)?;
|
||||
order::confirm_receipt(
|
||||
&state.pool,
|
||||
state.order_repo.as_ref(),
|
||||
&auth,
|
||||
&id,
|
||||
user_int_id,
|
||||
)
|
||||
.await?;
|
||||
state
|
||||
.order_service
|
||||
.confirm_receipt(&auth, &id, user_int_id)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -324,14 +300,10 @@ pub async fn admin_list(
|
||||
) -> AppResult<ApiResponse<crate::errors::response::PaginatedData<OrderResponse>>> {
|
||||
auth.ensure_admin()?;
|
||||
params.sanitize();
|
||||
let (orders, total) = order::list_admin_orders(
|
||||
state.order_repo.as_ref(),
|
||||
&auth,
|
||||
params.page,
|
||||
params.page_size,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let (orders, total) = state
|
||||
.order_service
|
||||
.list_admin(&auth, params.page, params.page_size, None)
|
||||
.await?;
|
||||
let mut responses = Vec::new();
|
||||
for o in orders {
|
||||
let items = state
|
||||
@@ -354,7 +326,7 @@ pub async fn admin_get(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<OrderResponse>> {
|
||||
auth.ensure_admin()?;
|
||||
let (o, items) = order::get_order(state.order_repo.as_ref(), &auth, &id).await?;
|
||||
let (o, items) = state.order_service.get(&auth, &id).await?;
|
||||
Ok(ApiResponse::success(to_order_response(o, items)))
|
||||
}
|
||||
|
||||
@@ -371,7 +343,7 @@ pub async fn admin_ship(
|
||||
Json(req): Json<ShipOrderRequest>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_admin()?;
|
||||
order::ship_order(&state.pool, state.order_repo.as_ref(), &auth, &id, &req).await?;
|
||||
state.order_service.ship(&auth, &id, &req).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -386,7 +358,7 @@ pub async fn admin_cancel(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_admin()?;
|
||||
order::admin_cancel(&state.pool, state.order_repo.as_ref(), &auth, &id).await?;
|
||||
state.order_service.admin_cancel(&auth, &id).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -401,7 +373,7 @@ pub async fn admin_pay(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_admin()?;
|
||||
order::mark_paid(&state.pool, state.order_repo.as_ref(), &auth, &id).await?;
|
||||
state.order_service.mark_paid(&auth, &id).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -416,7 +388,7 @@ pub async fn admin_refund(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_admin()?;
|
||||
order::refund_order(&state.pool, state.order_repo.as_ref(), &auth, &id).await?;
|
||||
state.order_service.refund(&auth, &id).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -433,7 +405,10 @@ pub async fn admin_update_remark(
|
||||
Json(req): Json<UpdateAdminRemarkRequest>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_admin()?;
|
||||
order::update_admin_remark(state.order_repo.as_ref(), &auth, &id, &req.admin_remark).await?;
|
||||
state
|
||||
.order_service
|
||||
.update_admin_remark(&auth, &id, &req.admin_remark)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -446,6 +421,6 @@ pub async fn admin_stats(
|
||||
State(state): State<crate::AppState>,
|
||||
) -> AppResult<ApiResponse<OrderStatsResponse>> {
|
||||
auth.ensure_admin()?;
|
||||
let stats = order::get_stats(&state.pool, &auth).await?;
|
||||
let stats = state.order_service.get_stats(&auth).await?;
|
||||
Ok(ApiResponse::success(stats))
|
||||
}
|
||||
|
||||
+26
-25
@@ -16,7 +16,7 @@ use crate::errors::response::{ApiResponse, PaginatedData};
|
||||
use crate::errors::validation;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::models::page::PageStatus;
|
||||
use crate::services::{page as page_service, post::resolve_doc_id_to_int};
|
||||
use crate::services::post::resolve_doc_id_to_int;
|
||||
use crate::utils::pagination::PaginationParams;
|
||||
|
||||
pub fn routes(
|
||||
@@ -248,9 +248,10 @@ pub async fn list(
|
||||
) -> AppResult<ApiResponse<PaginatedData<crate::models::page::Page>>> {
|
||||
let pagination = PaginationParams::from_options(query.page, query.page_size);
|
||||
|
||||
let (items, total) =
|
||||
page_service::list_published(&state.pool, pagination.page, pagination.page_size, &auth)
|
||||
.await?;
|
||||
let (items, total) = state
|
||||
.page_service
|
||||
.list_published(pagination.page, pagination.page_size, &auth)
|
||||
.await?;
|
||||
|
||||
Ok(pagination.paginate(items, total))
|
||||
}
|
||||
@@ -264,7 +265,7 @@ pub async fn get_by_slug(
|
||||
State(state): State<crate::AppState>,
|
||||
Path(slug): Path<String>,
|
||||
) -> AppResult<ApiResponse<crate::models::page::Page>> {
|
||||
let page = page_service::get_by_slug(&state.pool, &slug, &auth).await?;
|
||||
let page = state.page_service.get_by_slug(&slug, &auth).await?;
|
||||
Ok(ApiResponse::success(page))
|
||||
}
|
||||
|
||||
@@ -275,7 +276,9 @@ pub async fn sitemap(
|
||||
auth: AuthUser,
|
||||
State(state): State<crate::AppState>,
|
||||
) -> AppResult<ApiResponse<Vec<SitemapEntry>>> {
|
||||
let entries = page_service::sitemap(&state.pool, &auth)
|
||||
let entries = state
|
||||
.page_service
|
||||
.sitemap(&auth)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(slug, updated_at)| SitemapEntry { slug, updated_at })
|
||||
@@ -297,14 +300,10 @@ pub async fn admin_list(
|
||||
auth.ensure_author()?;
|
||||
let pagination = PaginationParams::from_options(query.page, query.page_size);
|
||||
|
||||
let (items, total) = page_service::list_all(
|
||||
&state.pool,
|
||||
pagination.page,
|
||||
pagination.page_size,
|
||||
query.status,
|
||||
&auth,
|
||||
)
|
||||
.await?;
|
||||
let (items, total) = state
|
||||
.page_service
|
||||
.list_all(pagination.page, pagination.page_size, query.status, &auth)
|
||||
.await?;
|
||||
|
||||
Ok(pagination.paginate(items, total))
|
||||
}
|
||||
@@ -320,7 +319,7 @@ pub async fn admin_get(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<crate::models::page::Page>> {
|
||||
auth.ensure_author()?;
|
||||
let page = page_service::get_by_id(&state.pool, &id, &auth).await?;
|
||||
let page = state.page_service.get_by_id(&id, &auth).await?;
|
||||
Ok(ApiResponse::success(page))
|
||||
}
|
||||
|
||||
@@ -360,7 +359,7 @@ pub async fn create(
|
||||
cover_image: req.cover_image,
|
||||
};
|
||||
|
||||
let page = page_service::create_page(&state.pool, &auth, cmd).await?;
|
||||
let page = state.page_service.create_page(&auth, cmd).await?;
|
||||
Ok(ApiResponse::success(page))
|
||||
}
|
||||
|
||||
@@ -396,7 +395,7 @@ pub async fn update(
|
||||
updated_by: auth.user_int_id(),
|
||||
};
|
||||
|
||||
let page = page_service::update_page(&state.pool, &auth, &id, cmd).await?;
|
||||
let page = state.page_service.update_page(&auth, &id, cmd).await?;
|
||||
Ok(ApiResponse::success(page))
|
||||
}
|
||||
|
||||
@@ -411,7 +410,7 @@ pub async fn delete(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_author()?;
|
||||
page_service::delete_page(&state.pool, &id, &auth).await?;
|
||||
state.page_service.delete_page(&id, &auth).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -427,7 +426,10 @@ pub async fn update_status(
|
||||
Json(req): Json<UpdateStatusRequest>,
|
||||
) -> AppResult<ApiResponse<crate::models::page::Page>> {
|
||||
auth.ensure_author()?;
|
||||
let page = page_service::update_status(&state.pool, &id, req.status, &auth).await?;
|
||||
let page = state
|
||||
.page_service
|
||||
.update_status(&id, req.status, &auth)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(page))
|
||||
}
|
||||
|
||||
@@ -446,7 +448,7 @@ pub async fn reorder(
|
||||
.into_iter()
|
||||
.map(|i| (i.id, i.sort_order))
|
||||
.collect();
|
||||
page_service::reorder(&state.pool, items, &auth).await?;
|
||||
state.page_service.reorder(items, &auth).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -466,10 +468,7 @@ pub async fn admin_batch(
|
||||
for id in &req.ids {
|
||||
match req.action.as_str() {
|
||||
"delete" => {
|
||||
if page_service::delete_page(&state.pool, id, &auth)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
if state.page_service.delete_page(id, &auth).await.is_ok() {
|
||||
affected += 1;
|
||||
}
|
||||
}
|
||||
@@ -479,7 +478,9 @@ pub async fn admin_batch(
|
||||
} else {
|
||||
PageStatus::Draft
|
||||
};
|
||||
if page_service::update_status(&state.pool, id, status, &auth)
|
||||
if state
|
||||
.page_service
|
||||
.update_status(id, status, &auth)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
|
||||
+69
-136
@@ -3,13 +3,11 @@ use axum::body::Bytes;
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::HeaderMap;
|
||||
|
||||
use crate::audit::AuditService;
|
||||
use crate::dto::payment::*;
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::errors::response::ApiResponse;
|
||||
use crate::errors::validation;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::services::payment;
|
||||
use crate::utils::pagination::PaginationParams;
|
||||
|
||||
pub fn routes(
|
||||
@@ -265,21 +263,17 @@ pub async fn create_payment_order_handler(
|
||||
let client_ip = extract_client_ip(&headers);
|
||||
let client_language = extract_accept_language(&headers);
|
||||
let client_user_agent = extract_user_agent(&headers);
|
||||
let (order, provider_resp) = payment::create_payment_order(
|
||||
&state.pool,
|
||||
state.payment_channel_repo.as_ref(),
|
||||
state.payment_order_repo.as_ref(),
|
||||
state.product_repo.as_ref(),
|
||||
state.order_repo.as_ref(),
|
||||
&auth,
|
||||
user_int_id,
|
||||
req,
|
||||
&state.config,
|
||||
client_ip.as_deref(),
|
||||
client_language.as_deref(),
|
||||
client_user_agent.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let (order, provider_resp) = state
|
||||
.payment_service
|
||||
.create_payment_order(
|
||||
&auth,
|
||||
user_int_id,
|
||||
req,
|
||||
client_ip.as_deref(),
|
||||
client_language.as_deref(),
|
||||
client_user_agent.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(to_order_response_with_provider(
|
||||
order,
|
||||
provider_resp,
|
||||
@@ -298,14 +292,10 @@ pub async fn list_user_orders(
|
||||
let _user_id = auth.ensure_authenticated()?;
|
||||
let user_int_id = auth.user_int_id().ok_or(AppError::Unauthorized)?;
|
||||
params.sanitize();
|
||||
let (orders, total) = payment::list_user_payment_orders(
|
||||
state.payment_order_repo.as_ref(),
|
||||
&auth,
|
||||
user_int_id,
|
||||
params.page,
|
||||
params.page_size,
|
||||
)
|
||||
.await?;
|
||||
let (orders, total) = state
|
||||
.payment_service
|
||||
.list_user_payment_orders(&auth, user_int_id, params.page, params.page_size)
|
||||
.await?;
|
||||
let responses: Vec<PaymentOrderResponse> = orders.into_iter().map(to_order_response).collect();
|
||||
Ok(params.paginate(responses, total))
|
||||
}
|
||||
@@ -322,9 +312,10 @@ pub async fn get_payment_order_handler(
|
||||
) -> AppResult<ApiResponse<PaymentOrderResponse>> {
|
||||
let _user_id = auth.ensure_authenticated()?;
|
||||
let user_int_id = auth.user_int_id().ok_or(AppError::Unauthorized)?;
|
||||
let order =
|
||||
payment::get_payment_order(state.payment_order_repo.as_ref(), &auth, user_int_id, &id)
|
||||
.await?;
|
||||
let order = state
|
||||
.payment_service
|
||||
.get_payment_order(&auth, user_int_id, &id)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(to_order_response(order)))
|
||||
}
|
||||
|
||||
@@ -340,18 +331,10 @@ pub async fn cancel_payment_order_handler(
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
let _user_id = auth.ensure_authenticated()?;
|
||||
let user_int_id = auth.user_int_id().ok_or(AppError::Unauthorized)?;
|
||||
let audit = AuditService::new(state.pool.clone());
|
||||
payment::cancel_payment_order(
|
||||
&state.pool,
|
||||
state.payment_order_repo.as_ref(),
|
||||
state.payment_channel_repo.as_ref(),
|
||||
&auth,
|
||||
&audit,
|
||||
&state.config,
|
||||
&id,
|
||||
user_int_id,
|
||||
)
|
||||
.await?;
|
||||
state
|
||||
.payment_service
|
||||
.cancel_payment_order(&auth, &id, user_int_id)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -367,12 +350,9 @@ pub async fn list_order_transactions(
|
||||
) -> AppResult<ApiResponse<Vec<PaymentTransactionResponse>>> {
|
||||
let _ = auth.ensure_authenticated()?;
|
||||
let user_int_id = auth.user_int_id().ok_or(AppError::Unauthorized)?;
|
||||
let order =
|
||||
payment::get_payment_order(state.payment_order_repo.as_ref(), &auth, user_int_id, &id)
|
||||
.await?;
|
||||
let txs = state
|
||||
.payment_tx_repo
|
||||
.find_by_payment_order_id(order.id, auth.tenant_id())
|
||||
.payment_service
|
||||
.list_order_transactions(&auth, user_int_id, &id)
|
||||
.await?;
|
||||
let responses: Vec<PaymentTransactionResponse> = txs.into_iter().map(Into::into).collect();
|
||||
Ok(ApiResponse::success(responses))
|
||||
@@ -390,12 +370,9 @@ pub async fn list_order_refunds(
|
||||
) -> AppResult<ApiResponse<Vec<PaymentRefundResponse>>> {
|
||||
let _ = auth.ensure_authenticated()?;
|
||||
let user_int_id = auth.user_int_id().ok_or(AppError::Unauthorized)?;
|
||||
let order =
|
||||
payment::get_payment_order(state.payment_order_repo.as_ref(), &auth, user_int_id, &id)
|
||||
.await?;
|
||||
let refunds = state
|
||||
.payment_refund_repo
|
||||
.find_by_payment_order_id(order.id, auth.tenant_id())
|
||||
.payment_service
|
||||
.list_order_refunds(&auth, user_int_id, &id)
|
||||
.await?;
|
||||
let responses: Vec<PaymentRefundResponse> = refunds.into_iter().map(Into::into).collect();
|
||||
Ok(ApiResponse::success(responses))
|
||||
@@ -412,20 +389,10 @@ pub async fn handle_callback(
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
let audit = AuditService::new(state.pool.clone());
|
||||
payment::handle_callback(
|
||||
&state.pool,
|
||||
state.payment_channel_repo.as_ref(),
|
||||
state.payment_order_repo.as_ref(),
|
||||
state.payment_tx_repo.as_ref(),
|
||||
state.wallet_repo.as_ref(),
|
||||
&audit,
|
||||
&state.config,
|
||||
&channel_doc_id,
|
||||
&headers,
|
||||
&body,
|
||||
)
|
||||
.await?;
|
||||
state
|
||||
.payment_service
|
||||
.handle_callback(&channel_doc_id, &headers, &body)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -467,15 +434,15 @@ pub async fn list_available_channels_handler(
|
||||
State(state): State<crate::AppState>,
|
||||
Query(query): Query<AvailableChannelsQuery>,
|
||||
) -> AppResult<ApiResponse<AvailableChannelsResponse>> {
|
||||
let result = payment::list_available_channels(
|
||||
state.payment_channel_repo.as_ref(),
|
||||
state.order_repo.as_ref(),
|
||||
&auth,
|
||||
&query.order_id,
|
||||
query.country.as_deref(),
|
||||
query.language.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let result = state
|
||||
.payment_service
|
||||
.list_available_channels(
|
||||
&auth,
|
||||
&query.order_id,
|
||||
query.country.as_deref(),
|
||||
query.language.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(result))
|
||||
}
|
||||
|
||||
@@ -491,8 +458,8 @@ pub async fn admin_list_channels(
|
||||
auth.ensure_admin()?;
|
||||
params.sanitize();
|
||||
let (channels, total) = state
|
||||
.payment_channel_repo
|
||||
.find_all_admin_paginated(auth.tenant_id(), params.page, params.page_size, None)
|
||||
.payment_service
|
||||
.list_admin_channels(&auth, params.page, params.page_size)
|
||||
.await?;
|
||||
let responses: Vec<PaymentChannelResponse> = channels.into_iter().map(Into::into).collect();
|
||||
Ok(params.paginate(responses, total))
|
||||
@@ -509,15 +476,7 @@ pub async fn admin_create_channel(
|
||||
Json(req): Json<CreatePaymentChannelRequest>,
|
||||
) -> AppResult<ApiResponse<PaymentChannelResponse>> {
|
||||
validation::validate(&req)?;
|
||||
let audit = AuditService::new(state.pool.clone());
|
||||
let channel = payment::create_channel(
|
||||
state.payment_channel_repo.as_ref(),
|
||||
&auth,
|
||||
&state.config,
|
||||
&audit,
|
||||
req,
|
||||
)
|
||||
.await?;
|
||||
let channel = state.payment_service.create_channel(&auth, req).await?;
|
||||
Ok(ApiResponse::success(PaymentChannelResponse::from(channel)))
|
||||
}
|
||||
|
||||
@@ -531,7 +490,7 @@ pub async fn admin_get_channel(
|
||||
State(state): State<crate::AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<PaymentChannelResponse>> {
|
||||
let channel = payment::get_channel(state.payment_channel_repo.as_ref(), &auth, &id).await?;
|
||||
let channel = state.payment_service.get_channel(&auth, &id).await?;
|
||||
Ok(ApiResponse::success(PaymentChannelResponse::from(channel)))
|
||||
}
|
||||
|
||||
@@ -548,16 +507,10 @@ pub async fn admin_update_channel(
|
||||
Json(req): Json<UpdatePaymentChannelRequest>,
|
||||
) -> AppResult<ApiResponse<PaymentChannelResponse>> {
|
||||
validation::validate(&req)?;
|
||||
let audit = AuditService::new(state.pool.clone());
|
||||
let channel = payment::update_channel(
|
||||
state.payment_channel_repo.as_ref(),
|
||||
&auth,
|
||||
&state.config,
|
||||
&audit,
|
||||
&id,
|
||||
req,
|
||||
)
|
||||
.await?;
|
||||
let channel = state
|
||||
.payment_service
|
||||
.update_channel(&auth, &id, req)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(PaymentChannelResponse::from(channel)))
|
||||
}
|
||||
|
||||
@@ -571,8 +524,7 @@ pub async fn admin_delete_channel(
|
||||
State(state): State<crate::AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
let audit = AuditService::new(state.pool.clone());
|
||||
payment::delete_channel(state.payment_channel_repo.as_ref(), &auth, &audit, &id).await?;
|
||||
state.payment_service.delete_channel(&auth, &id).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -587,14 +539,10 @@ pub async fn admin_list_orders(
|
||||
) -> AppResult<ApiResponse<crate::errors::response::PaginatedData<PaymentOrderResponse>>> {
|
||||
auth.ensure_admin()?;
|
||||
params.sanitize();
|
||||
let (orders, total) = payment::list_admin_payment_orders(
|
||||
state.payment_order_repo.as_ref(),
|
||||
&auth,
|
||||
params.page,
|
||||
params.page_size,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let (orders, total) = state
|
||||
.payment_service
|
||||
.list_admin_payment_orders(&auth, params.page, params.page_size, None)
|
||||
.await?;
|
||||
let responses: Vec<PaymentOrderResponse> = orders.into_iter().map(to_order_response).collect();
|
||||
Ok(params.paginate(responses, total))
|
||||
}
|
||||
@@ -610,8 +558,10 @@ pub async fn admin_get_order(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<PaymentOrderResponse>> {
|
||||
auth.ensure_admin()?;
|
||||
let order =
|
||||
payment::get_payment_order(state.payment_order_repo.as_ref(), &auth, 0, &id).await?;
|
||||
let order = state
|
||||
.payment_service
|
||||
.get_payment_order(&auth, 0, &id)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(to_order_response(order)))
|
||||
}
|
||||
|
||||
@@ -629,21 +579,10 @@ pub async fn admin_refund_order(
|
||||
) -> AppResult<ApiResponse<PaymentRefundResponse>> {
|
||||
auth.ensure_admin()?;
|
||||
validation::validate(&req)?;
|
||||
let audit = AuditService::new(state.pool.clone());
|
||||
let refund = payment::refund_payment_order(
|
||||
&state.pool,
|
||||
state.payment_order_repo.as_ref(),
|
||||
state.payment_channel_repo.as_ref(),
|
||||
state.payment_tx_repo.as_ref(),
|
||||
state.payment_refund_repo.as_ref(),
|
||||
state.wallet_repo.as_ref(),
|
||||
&auth,
|
||||
&audit,
|
||||
&state.config,
|
||||
&id,
|
||||
req,
|
||||
)
|
||||
.await?;
|
||||
let refund = state
|
||||
.payment_service
|
||||
.refund_payment_order(&auth, &id, req)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(PaymentRefundResponse::from(refund)))
|
||||
}
|
||||
|
||||
@@ -658,13 +597,10 @@ pub async fn admin_list_transactions(
|
||||
) -> AppResult<ApiResponse<crate::errors::response::PaginatedData<PaymentTransactionResponse>>> {
|
||||
auth.ensure_admin()?;
|
||||
params.sanitize();
|
||||
let (txs, total) = payment::list_admin_transactions(
|
||||
state.payment_tx_repo.as_ref(),
|
||||
&auth,
|
||||
params.page,
|
||||
params.page_size,
|
||||
)
|
||||
.await?;
|
||||
let (txs, total) = state
|
||||
.payment_service
|
||||
.list_admin_transactions(&auth, params.page, params.page_size)
|
||||
.await?;
|
||||
let responses: Vec<PaymentTransactionResponse> = txs.into_iter().map(Into::into).collect();
|
||||
Ok(params.paginate(responses, total))
|
||||
}
|
||||
@@ -680,13 +616,10 @@ pub async fn admin_list_refunds(
|
||||
) -> AppResult<ApiResponse<crate::errors::response::PaginatedData<PaymentRefundResponse>>> {
|
||||
auth.ensure_admin()?;
|
||||
params.sanitize();
|
||||
let (refunds, total) = payment::list_admin_refunds(
|
||||
state.payment_refund_repo.as_ref(),
|
||||
&auth,
|
||||
params.page,
|
||||
params.page_size,
|
||||
)
|
||||
.await?;
|
||||
let (refunds, total) = state
|
||||
.payment_service
|
||||
.list_admin_refunds(&auth, params.page, params.page_size)
|
||||
.await?;
|
||||
let responses: Vec<PaymentRefundResponse> = refunds.into_iter().map(Into::into).collect();
|
||||
Ok(params.paginate(responses, total))
|
||||
}
|
||||
|
||||
+12
-20
@@ -3,7 +3,6 @@ use crate::errors::app_error::AppResult;
|
||||
use crate::errors::response::ApiResponse;
|
||||
use crate::errors::validation;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::services::product;
|
||||
use crate::utils::pagination::PaginationParams;
|
||||
use axum::Json;
|
||||
use axum::extract::{Path, Query, State};
|
||||
@@ -86,13 +85,10 @@ pub async fn list_active(
|
||||
Query(mut params): Query<PaginationParams>,
|
||||
) -> AppResult<ApiResponse<crate::errors::response::PaginatedData<ProductResponse>>> {
|
||||
params.sanitize();
|
||||
let (items, total) = product::list_active_products(
|
||||
state.product_repo.as_ref(),
|
||||
&auth,
|
||||
params.page,
|
||||
params.page_size,
|
||||
)
|
||||
.await?;
|
||||
let (items, total) = state
|
||||
.product_service
|
||||
.list_active(&auth, params.page, params.page_size)
|
||||
.await?;
|
||||
let resp: Vec<ProductResponse> = items.into_iter().map(Into::into).collect();
|
||||
Ok(params.paginate(resp, total))
|
||||
}
|
||||
@@ -107,7 +103,7 @@ pub async fn get_product(
|
||||
State(state): State<crate::AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<ProductResponse>> {
|
||||
let p = product::get_product(state.product_repo.as_ref(), &id, &auth).await?;
|
||||
let p = state.product_service.get(&id, &auth).await?;
|
||||
Ok(ApiResponse::success(ProductResponse::from(p)))
|
||||
}
|
||||
|
||||
@@ -122,14 +118,10 @@ pub async fn admin_list(
|
||||
) -> AppResult<ApiResponse<crate::errors::response::PaginatedData<ProductResponse>>> {
|
||||
auth.ensure_admin()?;
|
||||
params.sanitize();
|
||||
let (items, total) = product::list_admin_products(
|
||||
state.product_repo.as_ref(),
|
||||
&auth,
|
||||
params.page,
|
||||
params.page_size,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let (items, total) = state
|
||||
.product_service
|
||||
.list_admin(&auth, params.page, params.page_size, None)
|
||||
.await?;
|
||||
let resp: Vec<ProductResponse> = items.into_iter().map(Into::into).collect();
|
||||
Ok(params.paginate(resp, total))
|
||||
}
|
||||
@@ -146,7 +138,7 @@ pub async fn admin_create(
|
||||
) -> AppResult<ApiResponse<ProductResponse>> {
|
||||
auth.ensure_admin()?;
|
||||
validation::validate(&req)?;
|
||||
let p = product::create_product(state.product_repo.as_ref(), &auth, req).await?;
|
||||
let p = state.product_service.create(&auth, req).await?;
|
||||
Ok(ApiResponse::success(ProductResponse::from(p)))
|
||||
}
|
||||
|
||||
@@ -164,7 +156,7 @@ pub async fn admin_update(
|
||||
) -> AppResult<ApiResponse<ProductResponse>> {
|
||||
auth.ensure_admin()?;
|
||||
validation::validate(&req)?;
|
||||
let p = product::update_product(state.product_repo.as_ref(), &auth, &id, req).await?;
|
||||
let p = state.product_service.update(&auth, &id, req).await?;
|
||||
Ok(ApiResponse::success(ProductResponse::from(p)))
|
||||
}
|
||||
|
||||
@@ -179,6 +171,6 @@ pub async fn admin_delete(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_admin()?;
|
||||
product::delete_product(state.product_repo.as_ref(), &id, &auth).await?;
|
||||
state.product_service.delete(&id, &auth).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
+24
-41
@@ -8,20 +8,8 @@ use crate::errors::app_error::AppResult;
|
||||
use crate::errors::response::{ApiResponse, PaginatedData};
|
||||
use crate::errors::validation;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::services::tag;
|
||||
use crate::utils::pagination::PaginationParams;
|
||||
|
||||
async fn dispatch_slug_for_tag(state: &crate::AppState, auth: &AuthUser, name: &str) -> String {
|
||||
match state
|
||||
.aspect_engine
|
||||
.before_create(&state.pool, "tags", auth, serde_json::json!({"name": name}))
|
||||
.await
|
||||
{
|
||||
Ok((_, d)) => d.str_or("slug", || tag::generate_slug(name)),
|
||||
Err(_) => tag::generate_slug(name),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes(
|
||||
registry: &mut crate::server::RouteRegistry,
|
||||
config: &crate::config::app::AppConfig,
|
||||
@@ -140,13 +128,10 @@ pub async fn list(
|
||||
Query(mut params): Query<PaginationParams>,
|
||||
) -> AppResult<ApiResponse<PaginatedData<crate::models::tag::Tag>>> {
|
||||
params.sanitize();
|
||||
let (items, total) = tag::list_tags_paginated(
|
||||
state.tag_repo.as_ref(),
|
||||
&auth,
|
||||
params.page,
|
||||
params.page_size,
|
||||
)
|
||||
.await?;
|
||||
let (items, total) = state
|
||||
.tag_service
|
||||
.list_paginated(&auth, params.page, params.page_size)
|
||||
.await?;
|
||||
Ok(params.paginate(items, total))
|
||||
}
|
||||
|
||||
@@ -160,7 +145,7 @@ pub async fn get(
|
||||
State(state): State<crate::AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<crate::models::tag::Tag>> {
|
||||
let t = tag::get_tag(state.tag_repo.as_ref(), &id, &auth).await?;
|
||||
let t = state.tag_service.get(&id, &auth).await?;
|
||||
Ok(ApiResponse::success(t))
|
||||
}
|
||||
|
||||
@@ -177,8 +162,7 @@ pub async fn create(
|
||||
) -> AppResult<ApiResponse<crate::models::tag::Tag>> {
|
||||
auth.ensure_author()?;
|
||||
validation::validate(&req)?;
|
||||
let slug = dispatch_slug_for_tag(&state, &auth, &req.name).await;
|
||||
let t = tag::create_tag(state.tag_repo.as_ref(), &auth, req, slug).await?;
|
||||
let t = state.tag_service.create(&auth, req).await?;
|
||||
Ok(ApiResponse::success(t))
|
||||
}
|
||||
|
||||
@@ -194,7 +178,7 @@ pub async fn delete(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_author()?;
|
||||
tag::delete_tag(state.tag_repo.as_ref(), &id, &auth).await?;
|
||||
state.tag_service.delete(&id, &auth).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -213,8 +197,11 @@ pub async fn update(
|
||||
) -> AppResult<ApiResponse<crate::models::tag::Tag>> {
|
||||
auth.ensure_author()?;
|
||||
validation::validate(&req)?;
|
||||
let slug = dispatch_slug_for_tag(&state, &auth, &req.name).await;
|
||||
let t = tag::update_tag(state.tag_repo.as_ref(), &id, &auth, req.name.clone(), slug).await?;
|
||||
let slug = crate::services::tag::generate_slug(&req.name);
|
||||
let t = state
|
||||
.tag_service
|
||||
.update(&auth, &id, req.name.clone(), slug)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(t))
|
||||
}
|
||||
|
||||
@@ -227,13 +214,10 @@ pub async fn admin_list(
|
||||
) -> AppResult<ApiResponse<PaginatedData<crate::models::tag::Tag>>> {
|
||||
auth.ensure_admin()?;
|
||||
params.sanitize();
|
||||
let (items, total) = tag::list_tags_paginated(
|
||||
state.tag_repo.as_ref(),
|
||||
&auth,
|
||||
params.page,
|
||||
params.page_size,
|
||||
)
|
||||
.await?;
|
||||
let (items, total) = state
|
||||
.tag_service
|
||||
.list_paginated(&auth, params.page, params.page_size)
|
||||
.await?;
|
||||
Ok(params.paginate(items, total))
|
||||
}
|
||||
|
||||
@@ -244,8 +228,7 @@ pub async fn admin_create(
|
||||
) -> AppResult<ApiResponse<crate::models::tag::Tag>> {
|
||||
auth.ensure_admin()?;
|
||||
validation::validate(&req)?;
|
||||
let slug = dispatch_slug_for_tag(&state, &auth, &req.name).await;
|
||||
let t = tag::create_tag(state.tag_repo.as_ref(), &auth, req, slug).await?;
|
||||
let t = state.tag_service.create(&auth, req).await?;
|
||||
Ok(ApiResponse::success(t))
|
||||
}
|
||||
|
||||
@@ -257,8 +240,11 @@ pub async fn admin_update(
|
||||
) -> AppResult<ApiResponse<crate::models::tag::Tag>> {
|
||||
auth.ensure_admin()?;
|
||||
validation::validate(&req)?;
|
||||
let slug = dispatch_slug_for_tag(&state, &auth, &req.name).await;
|
||||
let t = tag::update_tag(state.tag_repo.as_ref(), &id, &auth, req.name.clone(), slug).await?;
|
||||
let slug = crate::services::tag::generate_slug(&req.name);
|
||||
let t = state
|
||||
.tag_service
|
||||
.update(&auth, &id, req.name.clone(), slug)
|
||||
.await?;
|
||||
Ok(ApiResponse::success(t))
|
||||
}
|
||||
|
||||
@@ -268,7 +254,7 @@ pub async fn admin_delete(
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<ApiResponse<()>> {
|
||||
auth.ensure_admin()?;
|
||||
tag::delete_tag(state.tag_repo.as_ref(), &id, &auth).await?;
|
||||
state.tag_service.delete(&id, &auth).await?;
|
||||
Ok(ApiResponse::success(()))
|
||||
}
|
||||
|
||||
@@ -282,10 +268,7 @@ pub async fn admin_batch(
|
||||
let mut affected = 0usize;
|
||||
if req.action == "delete" {
|
||||
for id in &req.ids {
|
||||
if tag::delete_tag(state.tag_repo.as_ref(), id, &auth)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
if state.tag_service.delete(id, &auth).await.is_ok() {
|
||||
affected += 1;
|
||||
}
|
||||
}
|
||||
|
||||
+38
-46
@@ -203,8 +203,7 @@ pub async fn list_transactions(
|
||||
.find_transactions_by_wallet(w.id, params.page, params.page_size)
|
||||
.await?;
|
||||
|
||||
let items =
|
||||
crate::services::wallet::tx_list_to_response(state.wallet_repo.as_ref(), rows).await?;
|
||||
let items = state.wallet_service.tx_list_to_response(rows).await?;
|
||||
Ok(params.paginate(items, total))
|
||||
}
|
||||
|
||||
@@ -230,8 +229,7 @@ pub async fn list_all_transactions(
|
||||
.find_transactions_by_user(user.id, params.page, params.page_size)
|
||||
.await?;
|
||||
|
||||
let items =
|
||||
crate::services::wallet::tx_list_to_response(state.wallet_repo.as_ref(), rows).await?;
|
||||
let items = state.wallet_service.tx_list_to_response(rows).await?;
|
||||
Ok(params.paginate(items, total))
|
||||
}
|
||||
|
||||
@@ -275,8 +273,7 @@ pub async fn list_all_transactions_admin(
|
||||
.wallet_repo
|
||||
.find_all_transactions(params.page, params.page_size, auth.tenant_id())
|
||||
.await?;
|
||||
let items =
|
||||
crate::services::wallet::tx_list_to_response(state.wallet_repo.as_ref(), rows).await?;
|
||||
let items = state.wallet_service.tx_list_to_response(rows).await?;
|
||||
Ok(params.paginate(items, total))
|
||||
}
|
||||
|
||||
@@ -296,21 +293,21 @@ pub async fn admin_credit(
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("user"))?;
|
||||
|
||||
let tx = crate::services::wallet::credit_wallet(
|
||||
state.wallet_repo.as_ref(),
|
||||
&state.pool,
|
||||
user.id,
|
||||
&req.currency,
|
||||
req.amount,
|
||||
WalletTxType::Recharge,
|
||||
&req.transaction_no,
|
||||
req.reference_type.or(Some(WalletReferenceType::Admin)),
|
||||
req.reference_id.as_deref(),
|
||||
req.metadata.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let tx = state
|
||||
.wallet_service
|
||||
.credit(
|
||||
user.id,
|
||||
&req.currency,
|
||||
req.amount,
|
||||
WalletTxType::Recharge,
|
||||
&req.transaction_no,
|
||||
req.reference_type.or(Some(WalletReferenceType::Admin)),
|
||||
req.reference_id.as_deref(),
|
||||
req.metadata.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let resp = crate::services::wallet::tx_to_response(state.wallet_repo.as_ref(), tx).await?;
|
||||
let resp = state.wallet_service.tx_to_response(tx).await?;
|
||||
Ok(ApiResponse::success(resp))
|
||||
}
|
||||
|
||||
@@ -330,21 +327,21 @@ pub async fn admin_debit(
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("user"))?;
|
||||
|
||||
let tx = crate::services::wallet::debit_wallet(
|
||||
state.wallet_repo.as_ref(),
|
||||
&state.pool,
|
||||
user.id,
|
||||
&req.currency,
|
||||
req.amount,
|
||||
WalletTxType::Payment,
|
||||
&req.transaction_no,
|
||||
req.reference_type.or(Some(WalletReferenceType::Admin)),
|
||||
req.reference_id.as_deref(),
|
||||
req.metadata.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let tx = state
|
||||
.wallet_service
|
||||
.debit(
|
||||
user.id,
|
||||
&req.currency,
|
||||
req.amount,
|
||||
WalletTxType::Payment,
|
||||
&req.transaction_no,
|
||||
req.reference_type.or(Some(WalletReferenceType::Admin)),
|
||||
req.reference_id.as_deref(),
|
||||
req.metadata.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let resp = crate::services::wallet::tx_to_response(state.wallet_repo.as_ref(), tx).await?;
|
||||
let resp = state.wallet_service.tx_to_response(tx).await?;
|
||||
Ok(ApiResponse::success(resp))
|
||||
}
|
||||
|
||||
@@ -378,8 +375,7 @@ pub async fn list_user_transactions(
|
||||
.find_transactions_by_wallet(w.id, params.page, params.page_size)
|
||||
.await?;
|
||||
|
||||
let items =
|
||||
crate::services::wallet::tx_list_to_response(state.wallet_repo.as_ref(), rows).await?;
|
||||
let items = state.wallet_service.tx_list_to_response(rows).await?;
|
||||
Ok(params.paginate(items, total))
|
||||
}
|
||||
|
||||
@@ -407,8 +403,7 @@ pub async fn list_user_all_transactions(
|
||||
.find_transactions_by_user(user.id, params.page, params.page_size)
|
||||
.await?;
|
||||
|
||||
let items =
|
||||
crate::services::wallet::tx_list_to_response(state.wallet_repo.as_ref(), rows).await?;
|
||||
let items = state.wallet_service.tx_list_to_response(rows).await?;
|
||||
Ok(params.paginate(items, total))
|
||||
}
|
||||
|
||||
@@ -447,14 +442,11 @@ pub async fn admin_reversal(
|
||||
}
|
||||
}
|
||||
|
||||
let tx = crate::services::wallet::reverse_transaction(
|
||||
state.wallet_repo.as_ref(),
|
||||
&state.pool,
|
||||
original.id,
|
||||
&req.transaction_no,
|
||||
)
|
||||
.await?;
|
||||
let tx = state
|
||||
.wallet_service
|
||||
.reverse_transaction(original.id, &req.transaction_no)
|
||||
.await?;
|
||||
|
||||
let resp = crate::services::wallet::tx_to_response(state.wallet_repo.as_ref(), tx).await?;
|
||||
let resp = state.wallet_service.tx_to_response(tx).await?;
|
||||
Ok(ApiResponse::success(resp))
|
||||
}
|
||||
|
||||
+68
@@ -112,19 +112,27 @@ pub struct AppState {
|
||||
pub eventbus: EventBus,
|
||||
pub post_repo: Arc<dyn PostRepository>,
|
||||
pub post_service: Arc<dyn crate::services::post::PostService>,
|
||||
pub page_service: Arc<dyn crate::services::page::PageService>,
|
||||
pub user_repo: Arc<dyn UserRepository>,
|
||||
pub category_repo: Arc<dyn CategoryRepository>,
|
||||
pub category_service: Arc<dyn crate::services::category::CategoryService>,
|
||||
pub tag_repo: Arc<dyn TagRepository>,
|
||||
pub tag_service: Arc<dyn crate::services::tag::TagService>,
|
||||
pub comment_repo: Arc<dyn CommentRepository>,
|
||||
pub comment_service: Arc<dyn crate::services::comment::CommentService>,
|
||||
pub media_repo: Arc<dyn MediaRepository>,
|
||||
pub refresh_token_repo: Arc<dyn RefreshTokenRepository>,
|
||||
pub wallet_repo: Arc<dyn WalletRepository>,
|
||||
pub wallet_service: Arc<dyn crate::services::wallet::WalletService>,
|
||||
pub product_repo: Arc<dyn ProductRepository>,
|
||||
pub product_service: Arc<dyn crate::services::product::ProductService>,
|
||||
pub order_repo: Arc<dyn OrderRepository>,
|
||||
pub order_service: Arc<dyn crate::services::order::OrderService>,
|
||||
pub payment_channel_repo: Arc<dyn PaymentChannelRepository>,
|
||||
pub payment_order_repo: Arc<dyn PaymentOrderRepository>,
|
||||
pub payment_tx_repo: Arc<dyn PaymentTransactionRepository>,
|
||||
pub payment_refund_repo: Arc<dyn PaymentRefundRepository>,
|
||||
pub payment_service: Arc<dyn crate::services::payment::PaymentService>,
|
||||
pub search: Arc<dyn SearchEngine>,
|
||||
pub content_type_registry: Arc<ContentTypeRegistry>,
|
||||
pub aspect_engine: Arc<crate::aspects::engine::AspectEngine>,
|
||||
@@ -206,6 +214,35 @@ pub async fn build_app_state(
|
||||
|
||||
let aspect_engine = Arc::new(crate::aspects::engine::AspectEngine::new());
|
||||
|
||||
let order_service: Arc<dyn crate::services::order::OrderService> =
|
||||
Arc::new(crate::services::order::OrderServiceImpl::new(
|
||||
order_repo.clone(),
|
||||
product_repo.clone(),
|
||||
aspect_engine.clone(),
|
||||
Arc::new(pool.clone()),
|
||||
));
|
||||
|
||||
let wallet_service: Arc<dyn crate::services::wallet::WalletService> =
|
||||
Arc::new(crate::services::wallet::WalletServiceImpl::new(
|
||||
wallet_repo.clone(),
|
||||
aspect_engine.clone(),
|
||||
Arc::new(pool.clone()),
|
||||
));
|
||||
|
||||
let payment_service: Arc<dyn crate::services::payment::PaymentService> =
|
||||
Arc::new(crate::services::payment::PaymentServiceImpl::new(
|
||||
payment_channel_repo.clone(),
|
||||
payment_order_repo.clone(),
|
||||
payment_tx_repo.clone(),
|
||||
payment_refund_repo.clone(),
|
||||
order_repo.clone(),
|
||||
product_repo.clone(),
|
||||
wallet_repo.clone(),
|
||||
Arc::new(config.clone()),
|
||||
aspect_engine.clone(),
|
||||
Arc::new(pool.clone()),
|
||||
));
|
||||
|
||||
let reserved = config.builtins.reserved_route_segments();
|
||||
let protocol_names: Vec<&str> = protocol_registry.names();
|
||||
let ct_registry = Arc::new(ContentTypeRegistry::load_from_dir(
|
||||
@@ -250,6 +287,29 @@ pub async fn build_app_state(
|
||||
search.clone(),
|
||||
));
|
||||
|
||||
let tag_service: Arc<dyn crate::services::tag::TagService> = Arc::new(
|
||||
crate::services::tag::TagServiceImpl::new(tag_repo.clone(), aspect_engine.clone()),
|
||||
);
|
||||
let category_service: Arc<dyn crate::services::category::CategoryService> =
|
||||
Arc::new(crate::services::category::CategoryServiceImpl::new(
|
||||
category_repo.clone(),
|
||||
aspect_engine.clone(),
|
||||
));
|
||||
let page_service: Arc<dyn crate::services::page::PageService> = Arc::new(
|
||||
crate::services::page::PageServiceImpl::new(Arc::new(pool.clone()), aspect_engine.clone()),
|
||||
);
|
||||
let comment_service: Arc<dyn crate::services::comment::CommentService> =
|
||||
Arc::new(crate::services::comment::CommentServiceImpl::new(
|
||||
post_repo.clone(),
|
||||
comment_repo.clone(),
|
||||
aspect_engine.clone(),
|
||||
));
|
||||
let product_service: Arc<dyn crate::services::product::ProductService> =
|
||||
Arc::new(crate::services::product::ProductServiceImpl::new(
|
||||
product_repo.clone(),
|
||||
aspect_engine.clone(),
|
||||
));
|
||||
|
||||
let options_repo: Arc<dyn crate::repositories::OptionsRepository> = Arc::new(
|
||||
crate::repositories::SqlxOptionsRepository::new(pool.clone()),
|
||||
);
|
||||
@@ -297,19 +357,27 @@ pub async fn build_app_state(
|
||||
eventbus: eventbus.clone(),
|
||||
post_repo,
|
||||
post_service,
|
||||
page_service,
|
||||
user_repo,
|
||||
category_repo,
|
||||
category_service,
|
||||
tag_repo,
|
||||
tag_service,
|
||||
comment_repo,
|
||||
comment_service,
|
||||
media_repo,
|
||||
refresh_token_repo,
|
||||
wallet_repo,
|
||||
wallet_service,
|
||||
product_repo,
|
||||
product_service,
|
||||
order_repo,
|
||||
order_service,
|
||||
payment_channel_repo,
|
||||
payment_order_repo,
|
||||
payment_tx_repo,
|
||||
payment_refund_repo,
|
||||
payment_service,
|
||||
search,
|
||||
content_type_registry: ct_registry,
|
||||
aspect_engine,
|
||||
|
||||
+20
-69
@@ -617,57 +617,6 @@ async fn handle_plugin_route(
|
||||
}
|
||||
|
||||
/// Spawn EventBus background subscriber to forward business events to the plugin system.
|
||||
pub fn spawn_event_subscriber(
|
||||
eventbus: crate::eventbus::EventBus,
|
||||
plugins: Arc<crate::plugins::PluginManager>,
|
||||
mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
|
||||
) {
|
||||
use crate::eventbus::Event;
|
||||
|
||||
let mut rx = eventbus.subscribe();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = rx.recv() => {
|
||||
match result {
|
||||
Ok(event) => match event.as_ref() {
|
||||
Event::PostCreated(_) => {
|
||||
let json = serde_json::to_value(event.as_ref()).unwrap_or_default();
|
||||
plugins.dispatch_action("on_post_created", &json).await;
|
||||
}
|
||||
Event::PostUpdated(_) => {
|
||||
let json = serde_json::to_value(event.as_ref()).unwrap_or_default();
|
||||
plugins.dispatch_action("on_post_updated", &json).await;
|
||||
}
|
||||
Event::PostDeleted(_) => {
|
||||
let json = serde_json::to_value(event.as_ref()).unwrap_or_default();
|
||||
plugins.dispatch_action("on_post_deleted", &json).await;
|
||||
}
|
||||
Event::CommentCreated(_) => {
|
||||
let json = serde_json::to_value(event.as_ref()).unwrap_or_default();
|
||||
plugins
|
||||
.dispatch_action("on_comment_created", &json)
|
||||
.await;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
|
||||
tracing::warn!("eventbus subscriber lagged, skipped {n} events");
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = shutdown_rx.changed() => {
|
||||
tracing::info!("event subscriber shutting down");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Spawn audit log subscriber to write all business events to the `audit_log` table.
|
||||
pub fn spawn_audit_subscriber(
|
||||
eventbus: crate::eventbus::EventBus,
|
||||
@@ -800,14 +749,26 @@ pub fn spawn_audit_subscriber(
|
||||
});
|
||||
}
|
||||
|
||||
/// Derive webhook event type from event metadata.
|
||||
///
|
||||
/// Format: `{singular_table}.{action}` where action is extracted from `display_name()`
|
||||
/// by stripping the PascalCase table prefix (e.g., `PostCreated` → `post.created`).
|
||||
fn webhook_event_type(event: &crate::eventbus::Event) -> Option<String> {
|
||||
let table = event.table()?;
|
||||
let singular = table.strip_suffix('s').unwrap_or(table);
|
||||
let display = event.display_name();
|
||||
let prefix = singular[..1].to_uppercase() + &singular[1..];
|
||||
let action = display.strip_prefix(&prefix)?;
|
||||
let action = action[..1].to_ascii_lowercase() + &action[1..];
|
||||
Some(format!("{}.{}", singular, action))
|
||||
}
|
||||
|
||||
/// Spawn webhook event delivery subscriber
|
||||
pub fn spawn_webhook_subscriber(
|
||||
eventbus: crate::eventbus::EventBus,
|
||||
webhook_service: Arc<crate::webhook::WebhookService>,
|
||||
mut shutdown_rx: tokio::sync::watch::Receiver<bool>,
|
||||
) {
|
||||
use crate::eventbus::Event;
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
@@ -823,26 +784,15 @@ pub fn spawn_webhook_subscriber(
|
||||
result = rx.recv() => {
|
||||
match result {
|
||||
Ok(event) => {
|
||||
let event_type = match event.as_ref() {
|
||||
Event::PostCreated(_) => "post.created",
|
||||
Event::PostUpdated(_) => "post.updated",
|
||||
Event::PostDeleted(_) => "post.deleted",
|
||||
Event::CommentCreated(_) => "comment.created",
|
||||
Event::UserRegistered(_) => "user.registered",
|
||||
Event::UserLoggedIn { .. } => "user.loggedIn",
|
||||
Event::MediaUploaded(_) => "media.uploaded",
|
||||
Event::MediaDeleted(_) => "media.deleted",
|
||||
Event::PasswordResetRequested { .. } => "user.passwordResetRequested",
|
||||
Event::EmailVerificationRequested { .. } => {
|
||||
"user.emailVerificationRequested"
|
||||
}
|
||||
_ => continue,
|
||||
let event_type = match webhook_event_type(event.as_ref()) {
|
||||
Some(t) => t,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let payload_value = serde_json::to_value(event.as_ref()).unwrap_or_default();
|
||||
let timestamp = crate::utils::tz::now_utc();
|
||||
let webhook_payload = crate::webhook::model::WebhookPayload {
|
||||
event: event_type.to_string(),
|
||||
event: event_type.clone(),
|
||||
data: payload_value,
|
||||
timestamp,
|
||||
};
|
||||
@@ -866,7 +816,7 @@ pub fn spawn_webhook_subscriber(
|
||||
let events: Vec<String> =
|
||||
serde_json::from_str(&sub.events).unwrap_or_default();
|
||||
if !events.iter().any(|e| {
|
||||
e == event_type || e == "*" || event_type.starts_with(&format!("{e}."))
|
||||
e == &event_type || e == "*" || event_type.starts_with(&format!("{e}."))
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
@@ -877,6 +827,7 @@ pub fn spawn_webhook_subscriber(
|
||||
);
|
||||
let url = sub.url.clone();
|
||||
let body_clone = body.clone();
|
||||
let event_type = event_type.clone();
|
||||
let client = client.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = client
|
||||
|
||||
+11
-23
@@ -13,14 +13,13 @@ use chrono::Utc;
|
||||
use jsonwebtoken::{EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::aspects::engine::AspectEngine;
|
||||
use crate::dto::{LoginResponse, RegisterRequest, UpdatePasswordRequest, UserResponse};
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::event::Event;
|
||||
use crate::eventbus::EventBus;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::models::user::{UserRole, UserStatus};
|
||||
use crate::models::user_credential::AuthType;
|
||||
use crate::plugins::PluginManager;
|
||||
use crate::repositories::{RefreshTokenRepository, UserRepository};
|
||||
|
||||
/// JWT token claims (payload).
|
||||
@@ -170,10 +169,10 @@ pub fn generate_access_token_for_test(user_id: &str, user_int_id: i64, role: Use
|
||||
///
|
||||
/// Checks if the email is already registered; if unique, hashes the password and creates
|
||||
/// the user record and email credential within a transaction.
|
||||
#[tracing::instrument(skip(_user_repo, eventbus), fields(username = tracing::field::Empty))]
|
||||
#[tracing::instrument(skip(_user_repo, aspect_engine), fields(username = tracing::field::Empty))]
|
||||
pub async fn register(
|
||||
_user_repo: &dyn UserRepository,
|
||||
eventbus: &EventBus,
|
||||
aspect_engine: &AspectEngine,
|
||||
req: RegisterRequest,
|
||||
tenant_id: Option<&str>,
|
||||
require_email_verification: bool,
|
||||
@@ -286,11 +285,14 @@ pub async fn register(
|
||||
Ok::<_, crate::errors::app_error::AppError>(user)
|
||||
})?;
|
||||
|
||||
eventbus.emit(Event::UserRegistered(user.clone()));
|
||||
aspect_engine.emit(Event::UserRegistered(user.clone()));
|
||||
|
||||
if require_email_verification {
|
||||
let _ = crate::services::email_verification::trigger_email_verification(
|
||||
pool, eventbus, user.id, &req.email,
|
||||
pool,
|
||||
aspect_engine,
|
||||
user.id,
|
||||
&req.email,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -303,12 +305,11 @@ pub async fn register(
|
||||
/// Looks up the user via email credential, verifies the password, and on success generates
|
||||
/// an access token and a refresh token.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[tracing::instrument(skip(_user_repo, refresh_token_repo, plugins, eventbus), fields(email = %req.email))]
|
||||
#[tracing::instrument(skip(_user_repo, refresh_token_repo, aspect_engine), fields(email = %req.email))]
|
||||
pub async fn login(
|
||||
_user_repo: &dyn UserRepository,
|
||||
refresh_token_repo: &dyn RefreshTokenRepository,
|
||||
plugins: &PluginManager,
|
||||
eventbus: &EventBus,
|
||||
aspect_engine: &AspectEngine,
|
||||
pool: &crate::db::Pool,
|
||||
req: &crate::dto::LoginRequest,
|
||||
jwt_secret: &str,
|
||||
@@ -329,12 +330,6 @@ pub async fn login(
|
||||
&req.password,
|
||||
&crate::models::user_credential::extract_password_hash(&cred.credential_data)?,
|
||||
)? {
|
||||
plugins
|
||||
.dispatch_action(
|
||||
"on_login",
|
||||
&serde_json::json!({"email": &req.email, "success": false}),
|
||||
)
|
||||
.await;
|
||||
return Err(AppError::Unauthorized);
|
||||
}
|
||||
|
||||
@@ -375,14 +370,7 @@ pub async fn login(
|
||||
.create_token(user.id, &refresh_token_str, &expires_at.to_rfc3339())
|
||||
.await?;
|
||||
|
||||
plugins
|
||||
.dispatch_action(
|
||||
"on_login",
|
||||
&serde_json::json!({"email": &req.email, "success": true, "user_id": &user.document_id}),
|
||||
)
|
||||
.await;
|
||||
|
||||
eventbus.emit(Event::UserLoggedIn {
|
||||
aspect_engine.emit(Event::UserLoggedIn {
|
||||
user: user.clone(),
|
||||
success: true,
|
||||
});
|
||||
|
||||
+260
-177
@@ -1,129 +1,214 @@
|
||||
//! Category service.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::aspects::engine::AspectEngine;
|
||||
use crate::aspects::slug_aspect;
|
||||
use crate::commands::{CreateCategoryCmd, UpdateCategoryCmd};
|
||||
use crate::dto::{CreateCategoryRequest, UpdateCategoryRequest};
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::event::Event;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::models::category::Category;
|
||||
use crate::repositories::CategoryRepository;
|
||||
|
||||
pub async fn create_category(
|
||||
category_repo: &dyn CategoryRepository,
|
||||
auth: &AuthUser,
|
||||
req: CreateCategoryRequest,
|
||||
) -> AppResult<crate::models::category::Category> {
|
||||
let slug = slug_aspect::generate_slug(&req.name);
|
||||
let parent_id = if let Some(ref doc_id) = req.parent_id {
|
||||
if doc_id.parse::<i64>().is_ok() {
|
||||
doc_id.parse::<i64>().ok()
|
||||
} else {
|
||||
category_repo
|
||||
.find_by_document_id(doc_id, auth.tenant_id())
|
||||
.await?
|
||||
.map(|c| c.id)
|
||||
/// Category business logic trait.
|
||||
#[async_trait]
|
||||
pub trait CategoryService: Send + Sync {
|
||||
async fn create(&self, auth: &AuthUser, req: CreateCategoryRequest) -> AppResult<Category>;
|
||||
async fn update(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
id: &str,
|
||||
req: UpdateCategoryRequest,
|
||||
) -> AppResult<Category>;
|
||||
async fn delete(&self, id: &str, auth: &AuthUser) -> AppResult<()>;
|
||||
async fn get(&self, id: &str, auth: &AuthUser) -> AppResult<Category>;
|
||||
async fn list(&self, auth: &AuthUser) -> AppResult<Vec<Category>>;
|
||||
async fn list_paginated(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<Category>, i64)>;
|
||||
}
|
||||
|
||||
pub struct CategoryServiceImpl {
|
||||
repo: Arc<dyn CategoryRepository>,
|
||||
aspect_engine: Arc<AspectEngine>,
|
||||
}
|
||||
|
||||
impl CategoryServiceImpl {
|
||||
pub fn new(repo: Arc<dyn CategoryRepository>, aspect_engine: Arc<AspectEngine>) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
aspect_engine,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
category_repo
|
||||
.create(
|
||||
CreateCategoryCmd {
|
||||
name: req.name,
|
||||
slug,
|
||||
description: req.description,
|
||||
parent_id,
|
||||
sort_order: req.sort_order.unwrap_or(0),
|
||||
},
|
||||
auth.tenant_id(),
|
||||
auth.user_int_id(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn before_create(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
req: CreateCategoryRequest,
|
||||
) -> AppResult<(CreateCategoryRequest, crate::aspects::Dispatched)> {
|
||||
self.aspect_engine
|
||||
.before_create("categories", auth, req)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn before_update(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
existing: &Category,
|
||||
req: UpdateCategoryRequest,
|
||||
) -> AppResult<(UpdateCategoryRequest, crate::aspects::Dispatched)> {
|
||||
self.aspect_engine
|
||||
.before_update("categories", auth, existing, req)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn before_delete(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
existing: &Category,
|
||||
) -> AppResult<crate::aspects::Dispatched> {
|
||||
self.aspect_engine
|
||||
.before_delete("categories", auth, existing)
|
||||
.await
|
||||
}
|
||||
|
||||
fn after_created(&self, cat: &Category) {
|
||||
self.aspect_engine.emit(Event::CategoryCreated(cat.clone()));
|
||||
}
|
||||
|
||||
fn after_updated(&self, cat: &Category) {
|
||||
self.aspect_engine.emit(Event::CategoryUpdated(cat.clone()));
|
||||
}
|
||||
|
||||
fn after_deleted(&self, cat: &Category) {
|
||||
self.aspect_engine.emit(Event::CategoryDeleted(cat.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_category(
|
||||
category_repo: &dyn CategoryRepository,
|
||||
auth: &AuthUser,
|
||||
id: &str,
|
||||
req: UpdateCategoryRequest,
|
||||
) -> AppResult<crate::models::category::Category> {
|
||||
let existing = category_repo
|
||||
.find_by_document_id(id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("category"))?;
|
||||
let new_slug = req
|
||||
.name
|
||||
.as_ref()
|
||||
.map(|n| slug_aspect::generate_slug(n))
|
||||
.unwrap_or(existing.slug);
|
||||
|
||||
let parent_id = if let Some(ref doc_id) = req.parent_id {
|
||||
if doc_id.parse::<i64>().is_ok() {
|
||||
doc_id.parse::<i64>().ok()
|
||||
#[async_trait]
|
||||
impl CategoryService for CategoryServiceImpl {
|
||||
async fn create(&self, auth: &AuthUser, req: CreateCategoryRequest) -> AppResult<Category> {
|
||||
let (req, _d) = self.before_create(auth, req).await?;
|
||||
let slug = slug_aspect::generate_slug(&req.name);
|
||||
let parent_id = if let Some(ref doc_id) = req.parent_id {
|
||||
if doc_id.parse::<i64>().is_ok() {
|
||||
doc_id.parse::<i64>().ok()
|
||||
} else {
|
||||
self.repo
|
||||
.find_by_document_id(doc_id, auth.tenant_id())
|
||||
.await?
|
||||
.map(|c| c.id)
|
||||
}
|
||||
} else {
|
||||
category_repo
|
||||
.find_by_document_id(doc_id, auth.tenant_id())
|
||||
.await?
|
||||
.map(|c| c.id)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
category_repo
|
||||
.update(
|
||||
UpdateCategoryCmd {
|
||||
id: existing.id,
|
||||
name: req.name,
|
||||
slug: Some(new_slug),
|
||||
description: req.description,
|
||||
parent_id,
|
||||
sort_order: req.sort_order,
|
||||
},
|
||||
auth.tenant_id(),
|
||||
auth.user_int_id(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
None
|
||||
};
|
||||
let cat = self
|
||||
.repo
|
||||
.create(
|
||||
CreateCategoryCmd {
|
||||
name: req.name,
|
||||
slug,
|
||||
description: req.description,
|
||||
parent_id,
|
||||
sort_order: req.sort_order.unwrap_or(0),
|
||||
},
|
||||
auth.tenant_id(),
|
||||
auth.user_int_id(),
|
||||
)
|
||||
.await?;
|
||||
self.after_created(&cat);
|
||||
Ok(cat)
|
||||
}
|
||||
|
||||
pub async fn delete_category(
|
||||
category_repo: &dyn CategoryRepository,
|
||||
id: &str,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<()> {
|
||||
let existing = category_repo
|
||||
.find_by_document_id(id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("category"))?;
|
||||
category_repo.delete(existing.id, auth.tenant_id()).await?;
|
||||
Ok(())
|
||||
}
|
||||
async fn update(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
id: &str,
|
||||
req: UpdateCategoryRequest,
|
||||
) -> AppResult<Category> {
|
||||
let existing = self
|
||||
.repo
|
||||
.find_by_document_id(id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("category"))?;
|
||||
let (req, _d) = self.before_update(auth, &existing, req).await?;
|
||||
let new_slug = req
|
||||
.name
|
||||
.as_ref()
|
||||
.map(|n| slug_aspect::generate_slug(n))
|
||||
.unwrap_or(existing.slug);
|
||||
|
||||
pub async fn get_category(
|
||||
category_repo: &dyn CategoryRepository,
|
||||
id: &str,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<crate::models::category::Category> {
|
||||
category_repo
|
||||
.find_by_document_id(id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("category"))
|
||||
}
|
||||
let parent_id = if let Some(ref doc_id) = req.parent_id {
|
||||
if doc_id.parse::<i64>().is_ok() {
|
||||
doc_id.parse::<i64>().ok()
|
||||
} else {
|
||||
self.repo
|
||||
.find_by_document_id(doc_id, auth.tenant_id())
|
||||
.await?
|
||||
.map(|c| c.id)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let updated = self
|
||||
.repo
|
||||
.update(
|
||||
UpdateCategoryCmd {
|
||||
id: existing.id,
|
||||
name: req.name,
|
||||
slug: Some(new_slug),
|
||||
description: req.description,
|
||||
parent_id,
|
||||
sort_order: req.sort_order,
|
||||
},
|
||||
auth.tenant_id(),
|
||||
auth.user_int_id(),
|
||||
)
|
||||
.await?;
|
||||
self.after_updated(&updated);
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub async fn list_categories(
|
||||
category_repo: &dyn CategoryRepository,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<Vec<crate::models::category::Category>> {
|
||||
category_repo.find_all(auth.tenant_id()).await
|
||||
}
|
||||
async fn delete(&self, id: &str, auth: &AuthUser) -> AppResult<()> {
|
||||
let existing = self
|
||||
.repo
|
||||
.find_by_document_id(id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("category"))?;
|
||||
self.before_delete(auth, &existing).await?;
|
||||
self.repo.delete(existing.id, auth.tenant_id()).await?;
|
||||
self.after_deleted(&existing);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_categories_paginated(
|
||||
category_repo: &dyn CategoryRepository,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<crate::models::category::Category>, i64)> {
|
||||
category_repo
|
||||
.find_paginated(auth.tenant_id(), page, page_size)
|
||||
.await
|
||||
async fn get(&self, id: &str, auth: &AuthUser) -> AppResult<Category> {
|
||||
self.repo
|
||||
.find_by_document_id(id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("category"))
|
||||
}
|
||||
|
||||
async fn list(&self, auth: &AuthUser) -> AppResult<Vec<Category>> {
|
||||
self.repo.find_all(auth.tenant_id()).await
|
||||
}
|
||||
|
||||
async fn list_paginated(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<Category>, i64)> {
|
||||
self.repo
|
||||
.find_paginated(auth.tenant_id(), page, page_size)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -150,23 +235,30 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn make_service(pool: crate::db::Pool) -> Arc<dyn CategoryService> {
|
||||
Arc::new(CategoryServiceImpl::new(
|
||||
Arc::new(SqlxCategoryRepository::new(pool.clone())),
|
||||
Arc::new(AspectEngine::new()),
|
||||
))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_category_basic() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxCategoryRepository::new(pool.clone());
|
||||
let svc = make_service(pool.clone());
|
||||
let a = auth(None);
|
||||
let cat = super::create_category(
|
||||
&repo,
|
||||
&a,
|
||||
CreateCategoryRequest {
|
||||
name: "Tech".into(),
|
||||
description: Some("Technology".into()),
|
||||
parent_id: None,
|
||||
sort_order: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let cat = svc
|
||||
.create(
|
||||
&a,
|
||||
CreateCategoryRequest {
|
||||
name: "Tech".into(),
|
||||
description: Some("Technology".into()),
|
||||
parent_id: None,
|
||||
sort_order: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(cat.name, "Tech");
|
||||
assert_eq!(cat.slug, "tech");
|
||||
}
|
||||
@@ -174,42 +266,42 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn list_categories_empty() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxCategoryRepository::new(pool.clone());
|
||||
let svc = make_service(pool.clone());
|
||||
let a = auth(None);
|
||||
let cats = super::list_categories(&repo, &a).await.unwrap();
|
||||
let cats = svc.list(&a).await.unwrap();
|
||||
assert!(cats.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_category_renames() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxCategoryRepository::new(pool.clone());
|
||||
let svc = make_service(pool.clone());
|
||||
let a = auth(None);
|
||||
let cat = super::create_category(
|
||||
&repo,
|
||||
&a,
|
||||
CreateCategoryRequest {
|
||||
name: "Old".into(),
|
||||
description: None,
|
||||
parent_id: None,
|
||||
sort_order: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let updated = super::update_category(
|
||||
&repo,
|
||||
&a,
|
||||
&cat.document_id,
|
||||
crate::dto::UpdateCategoryRequest {
|
||||
name: Some("New".into()),
|
||||
description: None,
|
||||
parent_id: None,
|
||||
sort_order: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let cat = svc
|
||||
.create(
|
||||
&a,
|
||||
CreateCategoryRequest {
|
||||
name: "Old".into(),
|
||||
description: None,
|
||||
parent_id: None,
|
||||
sort_order: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let updated = svc
|
||||
.update(
|
||||
&a,
|
||||
&cat.document_id,
|
||||
crate::dto::UpdateCategoryRequest {
|
||||
name: Some("New".into()),
|
||||
description: None,
|
||||
parent_id: None,
|
||||
sort_order: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.name, "New");
|
||||
assert_eq!(updated.slug, "new");
|
||||
}
|
||||
@@ -217,47 +309,40 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn delete_category() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxCategoryRepository::new(pool.clone());
|
||||
let svc = make_service(pool.clone());
|
||||
let a = auth(None);
|
||||
let cat = super::create_category(
|
||||
&repo,
|
||||
&a,
|
||||
CreateCategoryRequest {
|
||||
name: "Del".into(),
|
||||
description: None,
|
||||
parent_id: None,
|
||||
sort_order: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
super::delete_category(&repo, &cat.document_id, &a)
|
||||
let cat = svc
|
||||
.create(
|
||||
&a,
|
||||
CreateCategoryRequest {
|
||||
name: "Del".into(),
|
||||
description: None,
|
||||
parent_id: None,
|
||||
sort_order: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let cats = super::list_categories(&repo, &a).await.unwrap();
|
||||
svc.delete(&cat.document_id, &a).await.unwrap();
|
||||
let cats = svc.list(&a).await.unwrap();
|
||||
assert!(cats.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_category_not_found() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxCategoryRepository::new(pool.clone());
|
||||
let svc = make_service(pool.clone());
|
||||
let a = auth(None);
|
||||
assert!(
|
||||
super::delete_category(&repo, "nonexistent", &a)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(svc.delete("nonexistent", &a).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_categories_paginated() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxCategoryRepository::new(pool.clone());
|
||||
let svc = make_service(pool.clone());
|
||||
let a = auth(None);
|
||||
for i in 0..5 {
|
||||
super::create_category(
|
||||
&repo,
|
||||
svc.create(
|
||||
&a,
|
||||
CreateCategoryRequest {
|
||||
name: format!("Cat{i}"),
|
||||
@@ -269,9 +354,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let (cats, total) = super::list_categories_paginated(&repo, &a, 1, 3)
|
||||
.await
|
||||
.unwrap();
|
||||
let (cats, total) = svc.list_paginated(&a, 1, 3).await.unwrap();
|
||||
assert_eq!(total, 5);
|
||||
assert_eq!(cats.len(), 3);
|
||||
}
|
||||
|
||||
+232
-137
@@ -3,13 +3,16 @@
|
||||
//! Handles comment-related business logic, including comment creation (with nesting depth validation),
|
||||
//! comment listing (tree structure), comment deletion, and status management.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::aspects::engine::AspectEngine;
|
||||
use crate::commands::CreateCommentCmd;
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::event::Event;
|
||||
use crate::eventbus::EventBus;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::models::comment::{self, CommentResponse, CommentStatus};
|
||||
use crate::plugins::PluginManager;
|
||||
use crate::policy::Policy;
|
||||
use crate::repositories::{CommentRepository, PostRepository};
|
||||
|
||||
@@ -21,149 +24,218 @@ pub struct CommentInput {
|
||||
pub parent_id: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn create_comment(
|
||||
post_repo: &dyn PostRepository,
|
||||
comment_repo: &dyn CommentRepository,
|
||||
plugins: &PluginManager,
|
||||
eventbus: &EventBus,
|
||||
post_slug: &str,
|
||||
auth: &AuthUser,
|
||||
content: &str,
|
||||
parent_id: Option<&str>,
|
||||
nickname: Option<&str>,
|
||||
email: Option<&str>,
|
||||
) -> AppResult<CommentResponse> {
|
||||
let p = post_repo
|
||||
.find_by_slug(post_slug, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("post"))?;
|
||||
/// Comment business logic trait.
|
||||
#[async_trait]
|
||||
pub trait CommentService: Send + Sync {
|
||||
async fn create(
|
||||
&self,
|
||||
post_slug: &str,
|
||||
auth: &AuthUser,
|
||||
content: &str,
|
||||
parent_id: Option<&str>,
|
||||
nickname: Option<&str>,
|
||||
email: Option<&str>,
|
||||
) -> AppResult<CommentResponse>;
|
||||
async fn list_paginated(
|
||||
&self,
|
||||
post_slug: &str,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<(Vec<CommentResponse>, i64)>;
|
||||
async fn delete(&self, comment_id: &str, auth: &AuthUser) -> AppResult<()>;
|
||||
async fn update_status(
|
||||
&self,
|
||||
comment_id: &str,
|
||||
status: CommentStatus,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<()>;
|
||||
}
|
||||
|
||||
if let Some(pid_str) = parent_id {
|
||||
let pid: i64 = pid_str
|
||||
.parse()
|
||||
.map_err(|_| AppError::BadRequest("invalid parent_id".into()))?;
|
||||
let all_comments = comment_repo
|
||||
.find_approved_by_post(p.id, auth.tenant_id())
|
||||
.await?;
|
||||
let parent = all_comments
|
||||
.iter()
|
||||
.find(|c| c.id == pid)
|
||||
.ok_or_else(|| AppError::not_found("parent_comment"))?;
|
||||
pub struct CommentServiceImpl {
|
||||
post_repo: Arc<dyn PostRepository>,
|
||||
comment_repo: Arc<dyn CommentRepository>,
|
||||
aspect_engine: Arc<AspectEngine>,
|
||||
}
|
||||
|
||||
if parent.post_id != p.id {
|
||||
return Err(AppError::BadRequest("parent_comment_mismatch".into()));
|
||||
impl CommentServiceImpl {
|
||||
pub fn new(
|
||||
post_repo: Arc<dyn PostRepository>,
|
||||
comment_repo: Arc<dyn CommentRepository>,
|
||||
aspect_engine: Arc<AspectEngine>,
|
||||
) -> Self {
|
||||
Self {
|
||||
post_repo,
|
||||
comment_repo,
|
||||
aspect_engine,
|
||||
}
|
||||
|
||||
comment::validate_depth(&all_comments, pid)?;
|
||||
}
|
||||
|
||||
let comment_input = CommentInput {
|
||||
content: content.to_string(),
|
||||
nickname: nickname.map(std::string::ToString::to_string),
|
||||
email: email.map(std::string::ToString::to_string),
|
||||
parent_id: parent_id.map(std::string::ToString::to_string),
|
||||
};
|
||||
async fn before_delete(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
existing: &crate::models::comment::Comment,
|
||||
) -> AppResult<crate::aspects::Dispatched> {
|
||||
crate::policy::CommentPolicy::can_delete(auth, existing)?;
|
||||
self.aspect_engine
|
||||
.before_delete("comments", auth, existing)
|
||||
.await
|
||||
}
|
||||
|
||||
let filtered = plugins
|
||||
.dispatch_filter(&Event::CommentCreating, comment_input)
|
||||
.await?;
|
||||
fn after_updated(&self, c: &crate::models::comment::Comment) {
|
||||
self.aspect_engine.emit(Event::CommentUpdated(c.clone()));
|
||||
}
|
||||
|
||||
let parent_id = if let Some(ref doc_id) = filtered.parent_id {
|
||||
if doc_id.is_empty() {
|
||||
None
|
||||
} else if let Ok(int_id) = doc_id.parse::<i64>() {
|
||||
Some(int_id)
|
||||
} else {
|
||||
comment_repo
|
||||
.find_by_document_id(doc_id, auth.tenant_id())
|
||||
.await?
|
||||
.map(|c| c.id)
|
||||
fn after_deleted(&self, c: &crate::models::comment::Comment) {
|
||||
self.aspect_engine.emit(Event::CommentDeleted(c.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CommentService for CommentServiceImpl {
|
||||
async fn create(
|
||||
&self,
|
||||
post_slug: &str,
|
||||
auth: &AuthUser,
|
||||
content: &str,
|
||||
parent_id: Option<&str>,
|
||||
nickname: Option<&str>,
|
||||
email: Option<&str>,
|
||||
) -> AppResult<CommentResponse> {
|
||||
let p = self
|
||||
.post_repo
|
||||
.find_by_slug(post_slug, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("post"))?;
|
||||
|
||||
if let Some(pid_str) = parent_id {
|
||||
let pid: i64 = pid_str
|
||||
.parse()
|
||||
.map_err(|_| AppError::BadRequest("invalid parent_id".into()))?;
|
||||
let all_comments = self
|
||||
.comment_repo
|
||||
.find_approved_by_post(p.id, auth.tenant_id())
|
||||
.await?;
|
||||
let parent = all_comments
|
||||
.iter()
|
||||
.find(|c| c.id == pid)
|
||||
.ok_or_else(|| AppError::not_found("parent_comment"))?;
|
||||
|
||||
if parent.post_id != p.id {
|
||||
return Err(AppError::BadRequest("parent_comment_mismatch".into()));
|
||||
}
|
||||
|
||||
comment::validate_depth(&all_comments, pid)?;
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let c = comment_repo
|
||||
.create(
|
||||
CreateCommentCmd {
|
||||
post_id: p.id,
|
||||
created_by: auth.user_int_id(),
|
||||
nickname: filtered.nickname,
|
||||
email: filtered.email,
|
||||
content: filtered.content,
|
||||
parent_id,
|
||||
},
|
||||
auth.tenant_id(),
|
||||
)
|
||||
.await?;
|
||||
let comment_input = CommentInput {
|
||||
content: content.to_string(),
|
||||
nickname: nickname.map(std::string::ToString::to_string),
|
||||
email: email.map(std::string::ToString::to_string),
|
||||
parent_id: parent_id.map(std::string::ToString::to_string),
|
||||
};
|
||||
|
||||
eventbus.emit(Event::CommentCreated(c.clone()));
|
||||
let (filtered, _d) = self
|
||||
.aspect_engine
|
||||
.before_create("comments", auth, comment_input)
|
||||
.await?;
|
||||
|
||||
Ok(CommentResponse {
|
||||
id: c.id,
|
||||
document_id: c.document_id.clone(),
|
||||
post_id: c.post_id,
|
||||
created_by: c.created_by,
|
||||
nickname: c.nickname,
|
||||
content: c.content,
|
||||
parent_id: c.parent_id,
|
||||
depth: 0,
|
||||
replies: vec![],
|
||||
created_at: c.created_at,
|
||||
})
|
||||
}
|
||||
let parent_id = if let Some(ref doc_id) = filtered.parent_id {
|
||||
if doc_id.is_empty() {
|
||||
None
|
||||
} else if let Ok(int_id) = doc_id.parse::<i64>() {
|
||||
Some(int_id)
|
||||
} else {
|
||||
self.comment_repo
|
||||
.find_by_document_id(doc_id, auth.tenant_id())
|
||||
.await?
|
||||
.map(|c| c.id)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
pub async fn list_comments_paginated(
|
||||
post_repo: &dyn PostRepository,
|
||||
comment_repo: &dyn CommentRepository,
|
||||
post_slug: &str,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<(Vec<CommentResponse>, i64)> {
|
||||
let p = post_repo
|
||||
.find_by_slug(post_slug, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("post"))?;
|
||||
let c = self
|
||||
.comment_repo
|
||||
.create(
|
||||
CreateCommentCmd {
|
||||
post_id: p.id,
|
||||
created_by: auth.user_int_id(),
|
||||
nickname: filtered.nickname,
|
||||
email: filtered.email,
|
||||
content: filtered.content,
|
||||
parent_id,
|
||||
},
|
||||
auth.tenant_id(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (comments, total) = comment_repo
|
||||
.find_approved_by_post_paginated(p.id, page, page_size, auth.tenant_id())
|
||||
.await?;
|
||||
Ok((comment::build_tree(&comments), total))
|
||||
}
|
||||
self.aspect_engine.emit(Event::CommentCreated(c.clone()));
|
||||
|
||||
pub async fn delete_comment(
|
||||
comment_repo: &dyn CommentRepository,
|
||||
comment_id: &str,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<()> {
|
||||
let c = comment_repo
|
||||
.find_by_document_id(comment_id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("comment"))?;
|
||||
Ok(CommentResponse {
|
||||
id: c.id,
|
||||
document_id: c.document_id.clone(),
|
||||
post_id: c.post_id,
|
||||
created_by: c.created_by,
|
||||
nickname: c.nickname,
|
||||
content: c.content,
|
||||
parent_id: c.parent_id,
|
||||
depth: 0,
|
||||
replies: vec![],
|
||||
created_at: c.created_at,
|
||||
})
|
||||
}
|
||||
|
||||
crate::policy::CommentPolicy::can_delete(auth, &c)?;
|
||||
async fn list_paginated(
|
||||
&self,
|
||||
post_slug: &str,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<(Vec<CommentResponse>, i64)> {
|
||||
let p = self
|
||||
.post_repo
|
||||
.find_by_slug(post_slug, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("post"))?;
|
||||
|
||||
comment_repo.delete(c.id, auth.tenant_id()).await?;
|
||||
Ok(())
|
||||
}
|
||||
let (comments, total) = self
|
||||
.comment_repo
|
||||
.find_approved_by_post_paginated(p.id, page, page_size, auth.tenant_id())
|
||||
.await?;
|
||||
Ok((comment::build_tree(&comments), total))
|
||||
}
|
||||
|
||||
pub async fn update_comment_status(
|
||||
comment_repo: &dyn CommentRepository,
|
||||
comment_id: &str,
|
||||
status: CommentStatus,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<()> {
|
||||
let c = comment_repo
|
||||
.find_by_document_id(comment_id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("comment"))?;
|
||||
comment_repo
|
||||
.update_status(c.id, status, auth.tenant_id())
|
||||
.await?;
|
||||
async fn delete(&self, comment_id: &str, auth: &AuthUser) -> AppResult<()> {
|
||||
let c = self
|
||||
.comment_repo
|
||||
.find_by_document_id(comment_id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("comment"))?;
|
||||
|
||||
Ok(())
|
||||
self.before_delete(auth, &c).await?;
|
||||
self.comment_repo.delete(c.id, auth.tenant_id()).await?;
|
||||
self.after_deleted(&c);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_status(
|
||||
&self,
|
||||
comment_id: &str,
|
||||
status: CommentStatus,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<()> {
|
||||
let c = self
|
||||
.comment_repo
|
||||
.find_by_document_id(comment_id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("comment"))?;
|
||||
self.comment_repo
|
||||
.update_status(c.id, status, auth.tenant_id())
|
||||
.await?;
|
||||
self.after_updated(&c);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -260,10 +332,15 @@ mod tests {
|
||||
let user = insert_user(&pool).await;
|
||||
let post_id = insert_post(&pool, user.id).await;
|
||||
let c = insert_comment(&pool, post_id, user.id).await;
|
||||
let repo = SqlxCommentRepository::new(pool.clone());
|
||||
super::update_comment_status(&repo, &c.document_id, CommentStatus::Approved, &auth(&user))
|
||||
let svc = CommentServiceImpl::new(
|
||||
Arc::new(crate::repositories::SqlxPostRepository::new(pool.clone())),
|
||||
Arc::new(SqlxCommentRepository::new(pool.clone())),
|
||||
Arc::new(AspectEngine::new()),
|
||||
);
|
||||
svc.update_status(&c.document_id, CommentStatus::Approved, &auth(&user))
|
||||
.await
|
||||
.unwrap();
|
||||
let repo = SqlxCommentRepository::new(pool.clone());
|
||||
let updated = repo.find_by_id(c.id, None).await.unwrap().unwrap();
|
||||
assert_eq!(
|
||||
updated.status,
|
||||
@@ -275,9 +352,14 @@ mod tests {
|
||||
async fn update_comment_status_not_found() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxCommentRepository::new(pool.clone());
|
||||
let svc = CommentServiceImpl::new(
|
||||
Arc::new(crate::repositories::SqlxPostRepository::new(pool.clone())),
|
||||
Arc::new(repo),
|
||||
Arc::new(AspectEngine::new()),
|
||||
);
|
||||
let a = AuthUser::new_test("any", crate::models::user::UserRole::Admin, "");
|
||||
assert!(
|
||||
super::update_comment_status(&repo, "missing", CommentStatus::Approved, &a)
|
||||
svc.update_status("missing", CommentStatus::Approved, &a)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
@@ -289,16 +371,19 @@ mod tests {
|
||||
let user = insert_user(&pool).await;
|
||||
let post_id = insert_post(&pool, user.id).await;
|
||||
let c = insert_comment(&pool, post_id, user.id).await;
|
||||
let repo = SqlxCommentRepository::new(pool.clone());
|
||||
let svc = CommentServiceImpl::new(
|
||||
Arc::new(crate::repositories::SqlxPostRepository::new(pool.clone())),
|
||||
Arc::new(SqlxCommentRepository::new(pool.clone())),
|
||||
Arc::new(AspectEngine::new()),
|
||||
);
|
||||
let a = AuthUser::from_parts(
|
||||
Some(user.document_id.clone()),
|
||||
Some(user.id),
|
||||
crate::models::user::UserRole::Admin,
|
||||
None,
|
||||
);
|
||||
super::delete_comment(&repo, &c.document_id, &a)
|
||||
.await
|
||||
.unwrap();
|
||||
svc.delete(&c.document_id, &a).await.unwrap();
|
||||
let repo = SqlxCommentRepository::new(pool.clone());
|
||||
assert!(repo.find_by_id(c.id, None).await.unwrap().is_none());
|
||||
}
|
||||
|
||||
@@ -306,8 +391,13 @@ mod tests {
|
||||
async fn delete_comment_not_found() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxCommentRepository::new(pool.clone());
|
||||
let svc = CommentServiceImpl::new(
|
||||
Arc::new(crate::repositories::SqlxPostRepository::new(pool.clone())),
|
||||
Arc::new(repo),
|
||||
Arc::new(AspectEngine::new()),
|
||||
);
|
||||
let a = AuthUser::new_test("any", crate::models::user::UserRole::Admin, "");
|
||||
assert!(super::delete_comment(&repo, "missing", &a).await.is_err());
|
||||
assert!(svc.delete("missing", &a).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -316,10 +406,15 @@ mod tests {
|
||||
let user = insert_user(&pool).await;
|
||||
let post_id = insert_post(&pool, user.id).await;
|
||||
let c = insert_comment(&pool, post_id, user.id).await;
|
||||
let repo = SqlxCommentRepository::new(pool.clone());
|
||||
super::update_comment_status(&repo, &c.document_id, CommentStatus::Spam, &auth(&user))
|
||||
let svc = CommentServiceImpl::new(
|
||||
Arc::new(crate::repositories::SqlxPostRepository::new(pool.clone())),
|
||||
Arc::new(SqlxCommentRepository::new(pool.clone())),
|
||||
Arc::new(AspectEngine::new()),
|
||||
);
|
||||
svc.update_status(&c.document_id, CommentStatus::Spam, &auth(&user))
|
||||
.await
|
||||
.unwrap();
|
||||
let repo = SqlxCommentRepository::new(pool.clone());
|
||||
let updated = repo.find_by_id(c.id, None).await.unwrap().unwrap();
|
||||
assert_eq!(updated.status, crate::models::comment::CommentStatus::Spam);
|
||||
}
|
||||
|
||||
@@ -2,17 +2,14 @@
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::aspects::engine::AspectEngine;
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::event::Event;
|
||||
use crate::eventbus::EventBus;
|
||||
use crate::repositories::UserRepository;
|
||||
|
||||
/// Trigger email verification after registration (if enabled in config).
|
||||
///
|
||||
/// Deletes old tokens, creates a new token, and sends a verification email via EventBus.
|
||||
pub async fn trigger_email_verification(
|
||||
pool: &crate::db::Pool,
|
||||
eventbus: &EventBus,
|
||||
aspect_engine: &AspectEngine,
|
||||
user_id: i64,
|
||||
email: &str,
|
||||
) -> AppResult<()> {
|
||||
@@ -21,7 +18,7 @@ pub async fn trigger_email_verification(
|
||||
let verification =
|
||||
crate::models::email_verification::create(pool, user_id, email, 86400).await?;
|
||||
|
||||
eventbus.emit(Event::EmailVerificationRequested {
|
||||
aspect_engine.emit(Event::EmailVerificationRequested {
|
||||
user_id,
|
||||
email: email.to_string(),
|
||||
token: verification,
|
||||
@@ -78,7 +75,7 @@ pub async fn verify_email(pool: &crate::db::Pool, token: &str) -> AppResult<()>
|
||||
pub async fn resend_verification(
|
||||
pool: &crate::db::Pool,
|
||||
_user_repo: &dyn UserRepository,
|
||||
eventbus: &EventBus,
|
||||
aspect_engine: &AspectEngine,
|
||||
email: &str,
|
||||
) -> AppResult<()> {
|
||||
let cred = crate::models::user_credential::find_by_auth_type_and_identifier(
|
||||
@@ -93,7 +90,7 @@ pub async fn resend_verification(
|
||||
return Err(AppError::BadRequest("email_already_verified".into()));
|
||||
}
|
||||
|
||||
trigger_email_verification(pool, eventbus, cred.user_id, &cred.identifier).await
|
||||
trigger_email_verification(pool, aspect_engine, cred.user_id, &cred.identifier).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -111,8 +108,8 @@ mod tests {
|
||||
pool
|
||||
}
|
||||
|
||||
fn eventbus() -> crate::eventbus::EventBus {
|
||||
crate::eventbus::EventBus::new(16)
|
||||
fn aspect_engine() -> crate::aspects::engine::AspectEngine {
|
||||
crate::aspects::engine::AspectEngine::new()
|
||||
}
|
||||
|
||||
async fn insert_user(pool: &crate::db::Pool, email: &str) -> crate::models::user::User {
|
||||
@@ -144,8 +141,8 @@ mod tests {
|
||||
async fn trigger_email_verification_creates_token() {
|
||||
let pool = setup_pool().await;
|
||||
let user = insert_user(&pool, "verify@test.com").await;
|
||||
let eb = eventbus();
|
||||
super::trigger_email_verification(&pool, &eb, user.id, "verify@test.com")
|
||||
let ae = aspect_engine();
|
||||
super::trigger_email_verification(&pool, &ae, user.id, "verify@test.com")
|
||||
.await
|
||||
.unwrap();
|
||||
let row =
|
||||
@@ -162,8 +159,8 @@ mod tests {
|
||||
async fn trigger_email_verification_replaces_old() {
|
||||
let pool = setup_pool().await;
|
||||
let user = insert_user(&pool, "replace@test.com").await;
|
||||
let eb = eventbus();
|
||||
super::trigger_email_verification(&pool, &eb, user.id, "replace@test.com")
|
||||
let ae = aspect_engine();
|
||||
super::trigger_email_verification(&pool, &ae, user.id, "replace@test.com")
|
||||
.await
|
||||
.unwrap();
|
||||
let sql = format!(
|
||||
@@ -176,7 +173,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(count_before, 1);
|
||||
super::trigger_email_verification(&pool, &eb, user.id, "replace@test.com")
|
||||
super::trigger_email_verification(&pool, &ae, user.id, "replace@test.com")
|
||||
.await
|
||||
.unwrap();
|
||||
let (count_after,): (i64,) = sqlx::query_as(&sql)
|
||||
@@ -191,8 +188,8 @@ mod tests {
|
||||
async fn verify_email_valid_token() {
|
||||
let pool = setup_pool().await;
|
||||
let user = insert_user(&pool, "v@test.com").await;
|
||||
let eb = eventbus();
|
||||
super::trigger_email_verification(&pool, &eb, user.id, "v@test.com")
|
||||
let ae = aspect_engine();
|
||||
super::trigger_email_verification(&pool, &ae, user.id, "v@test.com")
|
||||
.await
|
||||
.unwrap();
|
||||
let sql = format!(
|
||||
@@ -230,9 +227,9 @@ mod tests {
|
||||
async fn resend_verification_success() {
|
||||
let pool = setup_pool().await;
|
||||
let user = insert_user(&pool, "resend@test.com").await;
|
||||
let eb = eventbus();
|
||||
let ae = aspect_engine();
|
||||
let repo = SqlxUserRepository::new(pool.clone());
|
||||
super::resend_verification(&pool, &repo, &eb, "resend@test.com")
|
||||
super::resend_verification(&pool, &repo, &ae, "resend@test.com")
|
||||
.await
|
||||
.unwrap();
|
||||
let sql = format!(
|
||||
@@ -251,8 +248,8 @@ mod tests {
|
||||
async fn resend_verification_already_verified() {
|
||||
let pool = setup_pool().await;
|
||||
let user = insert_user(&pool, "verified@test.com").await;
|
||||
let eb = eventbus();
|
||||
super::trigger_email_verification(&pool, &eb, user.id, "verified@test.com")
|
||||
let ae = aspect_engine();
|
||||
super::trigger_email_verification(&pool, &ae, user.id, "verified@test.com")
|
||||
.await
|
||||
.unwrap();
|
||||
let sql = format!(
|
||||
@@ -266,7 +263,7 @@ mod tests {
|
||||
.unwrap();
|
||||
super::verify_email(&pool, &token_str).await.unwrap();
|
||||
let repo = SqlxUserRepository::new(pool.clone());
|
||||
let err = super::resend_verification(&pool, &repo, &eb, "verified@test.com")
|
||||
let err = super::resend_verification(&pool, &repo, &ae, "verified@test.com")
|
||||
.await
|
||||
.unwrap_err();
|
||||
let msg = err.to_string();
|
||||
@@ -276,10 +273,10 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn resend_verification_user_not_found() {
|
||||
let pool = setup_pool().await;
|
||||
let eb = eventbus();
|
||||
let ae = aspect_engine();
|
||||
let repo = SqlxUserRepository::new(pool.clone());
|
||||
assert!(
|
||||
super::resend_verification(&pool, &repo, &eb, "nope@no.com")
|
||||
super::resend_verification(&pool, &repo, &ae, "nope@no.com")
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
@@ -9,9 +9,11 @@ use chrono::Utc;
|
||||
#[cfg(feature = "export-types")]
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::aspects::engine::AspectEngine;
|
||||
use crate::commands::CreateUserCmd;
|
||||
use crate::dto::LoginResponse;
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::event::Event;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::models::oauth;
|
||||
use crate::oauth::{OAuthProviderRegistry, OAuthUserInfo};
|
||||
@@ -89,7 +91,7 @@ pub async fn handle_callback(
|
||||
jwt_secret: &str,
|
||||
jwt_access_expires: u64,
|
||||
jwt_refresh_expires: u64,
|
||||
eventbus: &crate::eventbus::EventBus,
|
||||
aspect_engine: &AspectEngine,
|
||||
) -> AppResult<OAuthCallbackResult> {
|
||||
let provider = registry.get(provider_name).ok_or_else(|| {
|
||||
AppError::BadRequest(format!("unsupported OAuth provider: {provider_name}"))
|
||||
@@ -173,7 +175,7 @@ pub async fn handle_callback(
|
||||
)
|
||||
.await?;
|
||||
|
||||
eventbus.emit(crate::eventbus::Event::UserLoggedIn {
|
||||
aspect_engine.emit(Event::UserLoggedIn {
|
||||
user: user.clone(),
|
||||
success: true,
|
||||
});
|
||||
@@ -182,7 +184,8 @@ pub async fn handle_callback(
|
||||
}
|
||||
}
|
||||
|
||||
let user = auto_register_user(pool, user_repo, provider_name, &user_info, eventbus).await?;
|
||||
let user =
|
||||
auto_register_user(pool, user_repo, provider_name, &user_info, aspect_engine).await?;
|
||||
|
||||
do_bind_oauth(pool, user.id, provider_name, &token_resp, &user_info).await?;
|
||||
|
||||
@@ -306,7 +309,7 @@ async fn auto_register_user(
|
||||
user_repo: &dyn UserRepository,
|
||||
provider_name: &str,
|
||||
user_info: &OAuthUserInfo,
|
||||
eventbus: &crate::eventbus::EventBus,
|
||||
aspect_engine: &AspectEngine,
|
||||
) -> AppResult<crate::models::user::User> {
|
||||
let base_username = user_info.display_name.clone().unwrap_or_else(|| {
|
||||
format!(
|
||||
@@ -360,7 +363,7 @@ async fn auto_register_user(
|
||||
.await?
|
||||
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("failed to fetch created user")))?;
|
||||
|
||||
eventbus.emit(crate::eventbus::Event::UserRegistered(user.clone()));
|
||||
aspect_engine.emit(Event::UserRegistered(user.clone()));
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
+671
-729
File diff suppressed because it is too large
Load Diff
+193
-98
@@ -2,120 +2,215 @@
|
||||
//!
|
||||
//! Provides full CRUD business logic for pages, including status management and block validation.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::aspects::engine::AspectEngine;
|
||||
use crate::commands::{CreatePageCmd, UpdatePageCmd};
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::event::Event;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::models::page::{self, PageStatus};
|
||||
use crate::models::page::{self, Page, PageStatus};
|
||||
|
||||
fn validate_blocks_json(blocks: &str) -> AppResult<Vec<page::PageBlock>> {
|
||||
serde_json::from_str(blocks)
|
||||
.map_err(|e| AppError::BadRequest(format!("invalid blocks JSON: {e}")))
|
||||
/// Page business logic trait.
|
||||
#[async_trait]
|
||||
pub trait PageService: Send + Sync {
|
||||
async fn list_published(
|
||||
&self,
|
||||
page_num: i64,
|
||||
page_size: i64,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<(Vec<Page>, i64)>;
|
||||
async fn get_by_slug(&self, slug: &str, auth: &AuthUser) -> AppResult<Page>;
|
||||
async fn get_by_id(&self, id: &str, auth: &AuthUser) -> AppResult<Page>;
|
||||
async fn list_all(
|
||||
&self,
|
||||
page_num: i64,
|
||||
page_size: i64,
|
||||
status: Option<PageStatus>,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<(Vec<Page>, i64)>;
|
||||
async fn create_page(&self, auth: &AuthUser, cmd: CreatePageCmd) -> AppResult<Page>;
|
||||
async fn update_page(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
document_id: &str,
|
||||
cmd: UpdatePageCmd,
|
||||
) -> AppResult<Page>;
|
||||
async fn delete_page(&self, id: &str, auth: &AuthUser) -> AppResult<()>;
|
||||
async fn update_status(&self, id: &str, status: PageStatus, auth: &AuthUser)
|
||||
-> AppResult<Page>;
|
||||
async fn reorder(&self, items: Vec<(String, i64)>, auth: &AuthUser) -> AppResult<()>;
|
||||
async fn sitemap(&self, auth: &AuthUser) -> AppResult<Vec<(String, Option<String>)>>;
|
||||
}
|
||||
|
||||
pub async fn list_published(
|
||||
pool: &crate::db::Pool,
|
||||
page_num: i64,
|
||||
page_size: i64,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<(Vec<page::Page>, i64)> {
|
||||
page::list_published(pool, page_num, page_size, auth.tenant_id()).await
|
||||
pub struct PageServiceImpl {
|
||||
pool: Arc<crate::db::Pool>,
|
||||
aspect_engine: Arc<AspectEngine>,
|
||||
}
|
||||
|
||||
pub async fn get_by_slug(
|
||||
pool: &crate::db::Pool,
|
||||
slug: &str,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<page::Page> {
|
||||
page::find_by_slug(pool, slug, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("page"))
|
||||
}
|
||||
|
||||
pub async fn get_by_id(pool: &crate::db::Pool, id: &str, auth: &AuthUser) -> AppResult<page::Page> {
|
||||
page::find_by_document_id(pool, id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("page"))
|
||||
}
|
||||
|
||||
pub async fn list_all(
|
||||
pool: &crate::db::Pool,
|
||||
page_num: i64,
|
||||
page_size: i64,
|
||||
status: Option<PageStatus>,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<(Vec<page::Page>, i64)> {
|
||||
page::list_all(pool, page_num, page_size, status, auth.tenant_id()).await
|
||||
}
|
||||
|
||||
pub async fn create_page(
|
||||
pool: &crate::db::Pool,
|
||||
auth: &AuthUser,
|
||||
cmd: CreatePageCmd,
|
||||
) -> AppResult<page::Page> {
|
||||
if let Some(ref blocks) = cmd.blocks {
|
||||
validate_blocks_json(blocks)?;
|
||||
impl PageServiceImpl {
|
||||
pub fn new(pool: Arc<crate::db::Pool>, aspect_engine: Arc<AspectEngine>) -> Self {
|
||||
Self {
|
||||
pool,
|
||||
aspect_engine,
|
||||
}
|
||||
}
|
||||
|
||||
page::create(pool, &cmd, auth.tenant_id()).await
|
||||
}
|
||||
|
||||
pub async fn update_page(
|
||||
pool: &crate::db::Pool,
|
||||
auth: &AuthUser,
|
||||
document_id: &str,
|
||||
mut cmd: UpdatePageCmd,
|
||||
) -> AppResult<page::Page> {
|
||||
if let Some(ref blocks) = cmd.blocks {
|
||||
validate_blocks_json(blocks)?;
|
||||
fn validate_blocks_json(blocks: &str) -> AppResult<Vec<page::PageBlock>> {
|
||||
serde_json::from_str(blocks)
|
||||
.map_err(|e| AppError::BadRequest(format!("invalid blocks JSON: {e}")))
|
||||
}
|
||||
|
||||
let existing = page::find_by_document_id(pool, document_id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("page"))?;
|
||||
async fn before_create(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
cmd: CreatePageCmd,
|
||||
) -> AppResult<(CreatePageCmd, crate::aspects::Dispatched)> {
|
||||
self.aspect_engine.before_create("pages", auth, cmd).await
|
||||
}
|
||||
|
||||
cmd.id = existing.id;
|
||||
page::update(pool, &cmd, auth.tenant_id()).await
|
||||
async fn before_update(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
existing: &Page,
|
||||
cmd: UpdatePageCmd,
|
||||
) -> AppResult<(UpdatePageCmd, crate::aspects::Dispatched)> {
|
||||
self.aspect_engine
|
||||
.before_update("pages", auth, existing, cmd)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn before_delete(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
existing: &Page,
|
||||
) -> AppResult<crate::aspects::Dispatched> {
|
||||
self.aspect_engine
|
||||
.before_delete("pages", auth, existing)
|
||||
.await
|
||||
}
|
||||
|
||||
fn after_created(&self, p: &Page) {
|
||||
self.aspect_engine.emit(Event::PageCreated(p.clone()));
|
||||
}
|
||||
|
||||
fn after_updated(&self, p: &Page) {
|
||||
self.aspect_engine.emit(Event::PageUpdated(p.clone()));
|
||||
}
|
||||
|
||||
fn after_deleted(&self, p: &Page) {
|
||||
self.aspect_engine.emit(Event::PageDeleted(p.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_page(pool: &crate::db::Pool, id: &str, auth: &AuthUser) -> AppResult<()> {
|
||||
let p = page::find_by_document_id(pool, id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("page"))?;
|
||||
page::delete(pool, p.id, auth.tenant_id()).await
|
||||
}
|
||||
#[async_trait]
|
||||
impl PageService for PageServiceImpl {
|
||||
async fn list_published(
|
||||
&self,
|
||||
page_num: i64,
|
||||
page_size: i64,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<(Vec<Page>, i64)> {
|
||||
page::list_published(&self.pool, page_num, page_size, auth.tenant_id()).await
|
||||
}
|
||||
|
||||
pub async fn update_status(
|
||||
pool: &crate::db::Pool,
|
||||
id: &str,
|
||||
status: PageStatus,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<page::Page> {
|
||||
let p = page::find_by_document_id(pool, id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("page"))?;
|
||||
page::update_status(pool, p.id, status, auth.user_int_id(), auth.tenant_id()).await
|
||||
}
|
||||
async fn get_by_slug(&self, slug: &str, auth: &AuthUser) -> AppResult<Page> {
|
||||
page::find_by_slug(&self.pool, slug, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("page"))
|
||||
}
|
||||
|
||||
pub async fn reorder(
|
||||
pool: &crate::db::Pool,
|
||||
items: Vec<(String, i64)>,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<()> {
|
||||
let mut resolved = Vec::new();
|
||||
for (doc_id, sort_order) in items {
|
||||
let p = page::find_by_document_id(pool, &doc_id, auth.tenant_id())
|
||||
async fn get_by_id(&self, id: &str, auth: &AuthUser) -> AppResult<Page> {
|
||||
page::find_by_document_id(&self.pool, id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("page"))
|
||||
}
|
||||
|
||||
async fn list_all(
|
||||
&self,
|
||||
page_num: i64,
|
||||
page_size: i64,
|
||||
status: Option<PageStatus>,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<(Vec<Page>, i64)> {
|
||||
page::list_all(&self.pool, page_num, page_size, status, auth.tenant_id()).await
|
||||
}
|
||||
|
||||
async fn create_page(&self, auth: &AuthUser, cmd: CreatePageCmd) -> AppResult<Page> {
|
||||
let (cmd, _d) = self.before_create(auth, cmd).await?;
|
||||
if let Some(ref blocks) = cmd.blocks {
|
||||
Self::validate_blocks_json(blocks)?;
|
||||
}
|
||||
let p = page::create(&self.pool, &cmd, auth.tenant_id()).await?;
|
||||
self.after_created(&p);
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
async fn update_page(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
document_id: &str,
|
||||
mut cmd: UpdatePageCmd,
|
||||
) -> AppResult<Page> {
|
||||
let existing = page::find_by_document_id(&self.pool, document_id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("page"))?;
|
||||
resolved.push((p.id, sort_order));
|
||||
}
|
||||
page::reorder(pool, &resolved, auth.tenant_id()).await
|
||||
}
|
||||
|
||||
pub async fn sitemap(
|
||||
pool: &crate::db::Pool,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<Vec<(String, Option<String>)>> {
|
||||
page::list_sitemap(pool, auth.tenant_id()).await
|
||||
cmd.id = existing.id;
|
||||
let (cmd, _d) = self.before_update(auth, &existing, cmd).await?;
|
||||
if let Some(ref blocks) = cmd.blocks {
|
||||
Self::validate_blocks_json(blocks)?;
|
||||
}
|
||||
let updated = page::update(&self.pool, &cmd, auth.tenant_id()).await?;
|
||||
self.after_updated(&updated);
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
async fn delete_page(&self, id: &str, auth: &AuthUser) -> AppResult<()> {
|
||||
let p = page::find_by_document_id(&self.pool, id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("page"))?;
|
||||
self.before_delete(auth, &p).await?;
|
||||
page::delete(&self.pool, p.id, auth.tenant_id()).await?;
|
||||
self.after_deleted(&p);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_status(
|
||||
&self,
|
||||
id: &str,
|
||||
status: PageStatus,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<Page> {
|
||||
let p = page::find_by_document_id(&self.pool, id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("page"))?;
|
||||
page::update_status(
|
||||
&self.pool,
|
||||
p.id,
|
||||
status,
|
||||
auth.user_int_id(),
|
||||
auth.tenant_id(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn reorder(&self, items: Vec<(String, i64)>, auth: &AuthUser) -> AppResult<()> {
|
||||
let mut resolved = Vec::new();
|
||||
for (doc_id, sort_order) in items {
|
||||
let p = page::find_by_document_id(&self.pool, &doc_id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("page"))?;
|
||||
resolved.push((p.id, sort_order));
|
||||
}
|
||||
page::reorder(&self.pool, &resolved, auth.tenant_id()).await
|
||||
}
|
||||
|
||||
async fn sitemap(&self, auth: &AuthUser) -> AppResult<Vec<(String, Option<String>)>> {
|
||||
page::list_sitemap(&self.pool, auth.tenant_id()).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -124,27 +219,27 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn validate_blocks_json_valid_empty() {
|
||||
let blocks = validate_blocks_json("[]").unwrap();
|
||||
let blocks = PageServiceImpl::validate_blocks_json("[]").unwrap();
|
||||
assert!(blocks.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_blocks_json_valid_richtext() {
|
||||
let json = r#"[{"type":"richtext","content":"hello"}]"#;
|
||||
let blocks = validate_blocks_json(json).unwrap();
|
||||
let blocks = PageServiceImpl::validate_blocks_json(json).unwrap();
|
||||
assert_eq!(blocks.len(), 1);
|
||||
assert!(matches!(blocks[0], page::PageBlock::Richtext { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_blocks_json_invalid() {
|
||||
let result = validate_blocks_json("not json");
|
||||
let result = PageServiceImpl::validate_blocks_json("not json");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_blocks_json_invalid_structure() {
|
||||
let result = validate_blocks_json(r#"[{"wrong":"field"}]"#);
|
||||
let result = PageServiceImpl::validate_blocks_json(r#"[{"wrong":"field"}]"#);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::aspects::engine::AspectEngine;
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::event::Event;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::repositories::UserRepository;
|
||||
|
||||
/// Request a password reset.
|
||||
///
|
||||
/// Finds the user, deletes old tokens, creates a new token, and triggers email sending via EventBus.
|
||||
/// Always returns success regardless of whether the user exists (to prevent email enumeration).
|
||||
pub async fn forgot_password(
|
||||
pool: &crate::db::Pool,
|
||||
_user_repo: &dyn UserRepository,
|
||||
eventbus: &crate::eventbus::EventBus,
|
||||
aspect_engine: &AspectEngine,
|
||||
email: &str,
|
||||
_tenant_id: Option<&str>,
|
||||
) -> AppResult<()> {
|
||||
@@ -37,7 +35,7 @@ pub async fn forgot_password(
|
||||
|
||||
let reset_token = crate::models::password_reset::create(pool, user.id, 3600).await?;
|
||||
|
||||
eventbus.emit(crate::eventbus::Event::PasswordResetRequested {
|
||||
aspect_engine.emit(Event::PasswordResetRequested {
|
||||
user: user.clone(),
|
||||
token: reset_token,
|
||||
});
|
||||
@@ -193,8 +191,8 @@ mod tests {
|
||||
pool
|
||||
}
|
||||
|
||||
fn eventbus() -> crate::eventbus::EventBus {
|
||||
crate::eventbus::EventBus::new(16)
|
||||
fn aspect_engine() -> crate::aspects::engine::AspectEngine {
|
||||
crate::aspects::engine::AspectEngine::new()
|
||||
}
|
||||
|
||||
async fn insert_user(pool: &crate::db::Pool, email: &str) -> crate::models::user::User {
|
||||
@@ -229,8 +227,8 @@ mod tests {
|
||||
let pool = setup_pool().await;
|
||||
let user = insert_user(&pool, "reset@test.com").await;
|
||||
let repo = SqlxUserRepository::new(pool.clone());
|
||||
let eb = eventbus();
|
||||
super::forgot_password(&pool, &repo, &eb, "reset@test.com", None)
|
||||
let ae = aspect_engine();
|
||||
super::forgot_password(&pool, &repo, &ae, "reset@test.com", None)
|
||||
.await
|
||||
.unwrap();
|
||||
let sql = format!(
|
||||
@@ -249,8 +247,8 @@ mod tests {
|
||||
async fn forgot_password_nonexistent_user_ok() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxUserRepository::new(pool.clone());
|
||||
let eb = eventbus();
|
||||
super::forgot_password(&pool, &repo, &eb, "noone@test.com", None)
|
||||
let ae = aspect_engine();
|
||||
super::forgot_password(&pool, &repo, &ae, "noone@test.com", None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
@@ -270,9 +268,9 @@ mod tests {
|
||||
async fn reset_password_weak_password() {
|
||||
let pool = setup_pool().await;
|
||||
let user = insert_user(&pool, "weak@test.com").await;
|
||||
let eb = eventbus();
|
||||
let ae = aspect_engine();
|
||||
let repo = SqlxUserRepository::new(pool.clone());
|
||||
super::forgot_password(&pool, &repo, &eb, "weak@test.com", None)
|
||||
super::forgot_password(&pool, &repo, &ae, "weak@test.com", None)
|
||||
.await
|
||||
.unwrap();
|
||||
let sql = format!(
|
||||
|
||||
+408
-8
@@ -1,3 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::aspects::engine::AspectEngine;
|
||||
use crate::audit::AuditService;
|
||||
use crate::commands::{
|
||||
CreatePaymentChannelCmd, CreatePaymentOrderCmd, CreatePaymentRefundCmd,
|
||||
@@ -6,6 +11,7 @@ use crate::commands::{
|
||||
use crate::config::app::AppConfig;
|
||||
use crate::dto::payment::*;
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::eventbus::Event;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::models::payment_channel::PaymentChannel;
|
||||
use crate::models::payment_order::{PaymentOrder, PaymentStatus};
|
||||
@@ -15,11 +21,406 @@ use crate::models::wallet_transaction::{WalletReferenceType, WalletTxType};
|
||||
use crate::payment::ProviderResponse;
|
||||
use crate::payment::routing::{RoutingContext, select_best_channel, select_channels};
|
||||
use crate::repositories::{
|
||||
PaymentChannelRepository, PaymentOrderRepository, PaymentRefundRepository,
|
||||
PaymentTransactionRepository, WalletRepository,
|
||||
OrderRepository, PaymentChannelRepository, PaymentOrderRepository, PaymentRefundRepository,
|
||||
PaymentTransactionRepository, ProductRepository, WalletRepository,
|
||||
};
|
||||
use base64::Engine;
|
||||
|
||||
#[async_trait]
|
||||
pub trait PaymentService: Send + Sync {
|
||||
async fn create_channel(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
req: CreatePaymentChannelRequest,
|
||||
) -> AppResult<PaymentChannel>;
|
||||
async fn update_channel(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
id: &str,
|
||||
req: UpdatePaymentChannelRequest,
|
||||
) -> AppResult<PaymentChannel>;
|
||||
async fn delete_channel(&self, auth: &AuthUser, id: &str) -> AppResult<()>;
|
||||
async fn get_channel(&self, auth: &AuthUser, id: &str) -> AppResult<PaymentChannel>;
|
||||
async fn list_channels(&self, auth: &AuthUser) -> AppResult<Vec<PaymentChannel>>;
|
||||
async fn list_available_channels(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
order_id: &str,
|
||||
country: Option<&str>,
|
||||
language: Option<&str>,
|
||||
) -> AppResult<AvailableChannelsResponse>;
|
||||
async fn create_payment_order(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
user_id: i64,
|
||||
req: CreatePaymentOrderRequest,
|
||||
client_ip: Option<&str>,
|
||||
client_language: Option<&str>,
|
||||
client_user_agent: Option<&str>,
|
||||
) -> AppResult<(PaymentOrder, Option<ProviderResponse>)>;
|
||||
async fn cancel_payment_order(&self, auth: &AuthUser, id: &str, user_id: i64) -> AppResult<()>;
|
||||
async fn get_payment_order(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
user_id: i64,
|
||||
id: &str,
|
||||
) -> AppResult<PaymentOrder>;
|
||||
async fn list_user_payment_orders(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
user_id: i64,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<PaymentOrder>, i64)>;
|
||||
async fn handle_callback(
|
||||
&self,
|
||||
channel_doc_id: &str,
|
||||
headers: &axum::http::HeaderMap,
|
||||
body: &[u8],
|
||||
) -> AppResult<PaymentOrder>;
|
||||
async fn refund_payment_order(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
id: &str,
|
||||
req: CreateRefundRequest,
|
||||
) -> AppResult<PaymentRefund>;
|
||||
async fn list_admin_payment_orders(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
status: Option<&str>,
|
||||
) -> AppResult<(Vec<PaymentOrder>, i64)>;
|
||||
async fn list_admin_transactions(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<PaymentTransaction>, i64)>;
|
||||
async fn list_admin_refunds(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<PaymentRefund>, i64)>;
|
||||
async fn list_admin_channels(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<PaymentChannel>, i64)>;
|
||||
async fn list_order_transactions(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
user_id: i64,
|
||||
order_id: &str,
|
||||
) -> AppResult<Vec<PaymentTransaction>>;
|
||||
async fn list_order_refunds(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
user_id: i64,
|
||||
order_id: &str,
|
||||
) -> AppResult<Vec<PaymentRefund>>;
|
||||
}
|
||||
|
||||
pub struct PaymentServiceImpl {
|
||||
channel_repo: Arc<dyn PaymentChannelRepository>,
|
||||
order_repo: Arc<dyn PaymentOrderRepository>,
|
||||
tx_repo: Arc<dyn PaymentTransactionRepository>,
|
||||
refund_repo: Arc<dyn PaymentRefundRepository>,
|
||||
shop_order_repo: Arc<dyn OrderRepository>,
|
||||
product_repo: Arc<dyn ProductRepository>,
|
||||
wallet_repo: Arc<dyn WalletRepository>,
|
||||
config: Arc<AppConfig>,
|
||||
aspect_engine: Arc<AspectEngine>,
|
||||
pool: Arc<crate::db::Pool>,
|
||||
}
|
||||
|
||||
impl PaymentServiceImpl {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
channel_repo: Arc<dyn PaymentChannelRepository>,
|
||||
order_repo: Arc<dyn PaymentOrderRepository>,
|
||||
tx_repo: Arc<dyn PaymentTransactionRepository>,
|
||||
refund_repo: Arc<dyn PaymentRefundRepository>,
|
||||
shop_order_repo: Arc<dyn OrderRepository>,
|
||||
product_repo: Arc<dyn ProductRepository>,
|
||||
wallet_repo: Arc<dyn WalletRepository>,
|
||||
config: Arc<AppConfig>,
|
||||
aspect_engine: Arc<AspectEngine>,
|
||||
pool: Arc<crate::db::Pool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
channel_repo,
|
||||
order_repo,
|
||||
tx_repo,
|
||||
refund_repo,
|
||||
shop_order_repo,
|
||||
product_repo,
|
||||
wallet_repo,
|
||||
config,
|
||||
aspect_engine,
|
||||
pool,
|
||||
}
|
||||
}
|
||||
|
||||
fn after_payment_order_created(&self, order: &PaymentOrder) {
|
||||
self.aspect_engine
|
||||
.emit(Event::PaymentOrderCreated(order.clone()));
|
||||
}
|
||||
|
||||
fn after_payment_paid(&self, order: &PaymentOrder) {
|
||||
self.aspect_engine.emit(Event::PaymentPaid(order.clone()));
|
||||
}
|
||||
|
||||
fn after_payment_refunded(&self, order: &PaymentOrder) {
|
||||
self.aspect_engine
|
||||
.emit(Event::PaymentRefunded(order.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PaymentService for PaymentServiceImpl {
|
||||
async fn create_channel(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
req: CreatePaymentChannelRequest,
|
||||
) -> AppResult<PaymentChannel> {
|
||||
let audit = AuditService::new((*self.pool).clone());
|
||||
create_channel(self.channel_repo.as_ref(), auth, &self.config, &audit, req).await
|
||||
}
|
||||
|
||||
async fn update_channel(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
id: &str,
|
||||
req: UpdatePaymentChannelRequest,
|
||||
) -> AppResult<PaymentChannel> {
|
||||
let audit = AuditService::new((*self.pool).clone());
|
||||
update_channel(
|
||||
self.channel_repo.as_ref(),
|
||||
auth,
|
||||
&self.config,
|
||||
&audit,
|
||||
id,
|
||||
req,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn delete_channel(&self, auth: &AuthUser, id: &str) -> AppResult<()> {
|
||||
let audit = AuditService::new((*self.pool).clone());
|
||||
delete_channel(self.channel_repo.as_ref(), auth, &audit, id).await
|
||||
}
|
||||
|
||||
async fn get_channel(&self, auth: &AuthUser, id: &str) -> AppResult<PaymentChannel> {
|
||||
get_channel(self.channel_repo.as_ref(), auth, id).await
|
||||
}
|
||||
|
||||
async fn list_channels(&self, auth: &AuthUser) -> AppResult<Vec<PaymentChannel>> {
|
||||
list_channels(self.channel_repo.as_ref(), auth).await
|
||||
}
|
||||
|
||||
async fn list_available_channels(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
order_id: &str,
|
||||
country: Option<&str>,
|
||||
language: Option<&str>,
|
||||
) -> AppResult<AvailableChannelsResponse> {
|
||||
list_available_channels(
|
||||
self.channel_repo.as_ref(),
|
||||
self.shop_order_repo.as_ref(),
|
||||
auth,
|
||||
order_id,
|
||||
country,
|
||||
language,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn create_payment_order(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
user_id: i64,
|
||||
req: CreatePaymentOrderRequest,
|
||||
client_ip: Option<&str>,
|
||||
client_language: Option<&str>,
|
||||
client_user_agent: Option<&str>,
|
||||
) -> AppResult<(PaymentOrder, Option<ProviderResponse>)> {
|
||||
let (order, resp) = create_payment_order(
|
||||
&self.pool,
|
||||
self.channel_repo.as_ref(),
|
||||
self.order_repo.as_ref(),
|
||||
self.product_repo.as_ref(),
|
||||
self.shop_order_repo.as_ref(),
|
||||
auth,
|
||||
user_id,
|
||||
req,
|
||||
&self.config,
|
||||
client_ip,
|
||||
client_language,
|
||||
client_user_agent,
|
||||
)
|
||||
.await?;
|
||||
self.after_payment_order_created(&order);
|
||||
Ok((order, resp))
|
||||
}
|
||||
|
||||
async fn cancel_payment_order(&self, auth: &AuthUser, id: &str, user_id: i64) -> AppResult<()> {
|
||||
let audit = AuditService::new((*self.pool).clone());
|
||||
cancel_payment_order(
|
||||
&self.pool,
|
||||
self.order_repo.as_ref(),
|
||||
self.channel_repo.as_ref(),
|
||||
auth,
|
||||
&audit,
|
||||
&self.config,
|
||||
id,
|
||||
user_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_payment_order(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
user_id: i64,
|
||||
id: &str,
|
||||
) -> AppResult<PaymentOrder> {
|
||||
get_payment_order(self.order_repo.as_ref(), auth, user_id, id).await
|
||||
}
|
||||
|
||||
async fn list_user_payment_orders(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
user_id: i64,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<PaymentOrder>, i64)> {
|
||||
list_user_payment_orders(self.order_repo.as_ref(), auth, user_id, page, page_size).await
|
||||
}
|
||||
|
||||
async fn handle_callback(
|
||||
&self,
|
||||
channel_doc_id: &str,
|
||||
headers: &axum::http::HeaderMap,
|
||||
body: &[u8],
|
||||
) -> AppResult<PaymentOrder> {
|
||||
let audit = AuditService::new((*self.pool).clone());
|
||||
let order = handle_callback(
|
||||
&self.pool,
|
||||
self.channel_repo.as_ref(),
|
||||
self.order_repo.as_ref(),
|
||||
self.tx_repo.as_ref(),
|
||||
self.wallet_repo.as_ref(),
|
||||
&audit,
|
||||
&self.config,
|
||||
channel_doc_id,
|
||||
headers,
|
||||
body,
|
||||
)
|
||||
.await?;
|
||||
self.after_payment_paid(&order);
|
||||
Ok(order)
|
||||
}
|
||||
|
||||
async fn refund_payment_order(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
id: &str,
|
||||
req: CreateRefundRequest,
|
||||
) -> AppResult<PaymentRefund> {
|
||||
let audit = AuditService::new((*self.pool).clone());
|
||||
let refund = refund_payment_order(
|
||||
&self.pool,
|
||||
self.order_repo.as_ref(),
|
||||
self.channel_repo.as_ref(),
|
||||
self.tx_repo.as_ref(),
|
||||
self.refund_repo.as_ref(),
|
||||
self.wallet_repo.as_ref(),
|
||||
auth,
|
||||
&audit,
|
||||
&self.config,
|
||||
id,
|
||||
req,
|
||||
)
|
||||
.await?;
|
||||
if let Ok(Some(order)) = self
|
||||
.order_repo
|
||||
.find_by_document_id(id, auth.tenant_id())
|
||||
.await
|
||||
{
|
||||
self.after_payment_refunded(&order);
|
||||
}
|
||||
Ok(refund)
|
||||
}
|
||||
|
||||
async fn list_admin_payment_orders(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
status: Option<&str>,
|
||||
) -> AppResult<(Vec<PaymentOrder>, i64)> {
|
||||
list_admin_payment_orders(self.order_repo.as_ref(), auth, page, page_size, status).await
|
||||
}
|
||||
|
||||
async fn list_admin_transactions(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<PaymentTransaction>, i64)> {
|
||||
list_admin_transactions(self.tx_repo.as_ref(), auth, page, page_size).await
|
||||
}
|
||||
|
||||
async fn list_admin_refunds(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<PaymentRefund>, i64)> {
|
||||
list_admin_refunds(self.refund_repo.as_ref(), auth, page, page_size).await
|
||||
}
|
||||
|
||||
async fn list_admin_channels(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<PaymentChannel>, i64)> {
|
||||
auth.ensure_admin()?;
|
||||
self.channel_repo
|
||||
.find_all_admin_paginated(auth.tenant_id(), page, page_size, None)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_order_transactions(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
user_id: i64,
|
||||
order_id: &str,
|
||||
) -> AppResult<Vec<PaymentTransaction>> {
|
||||
let order = self.get_payment_order(auth, user_id, order_id).await?;
|
||||
self.tx_repo
|
||||
.find_by_payment_order_id(order.id, auth.tenant_id())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn list_order_refunds(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
user_id: i64,
|
||||
order_id: &str,
|
||||
) -> AppResult<Vec<PaymentRefund>> {
|
||||
let order = self.get_payment_order(auth, user_id, order_id).await?;
|
||||
self.refund_repo
|
||||
.find_by_payment_order_id(order.id, auth.tenant_id())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn is_unique_violation(err: &AppError) -> bool {
|
||||
match err {
|
||||
AppError::Internal(e) => {
|
||||
@@ -514,7 +915,7 @@ pub async fn handle_callback(
|
||||
channel_doc_id: &str,
|
||||
headers: &axum::http::HeaderMap,
|
||||
body: &[u8],
|
||||
) -> AppResult<()> {
|
||||
) -> AppResult<PaymentOrder> {
|
||||
let channel = channel_repo
|
||||
.find_by_document_id(channel_doc_id, None)
|
||||
.await?
|
||||
@@ -555,7 +956,7 @@ pub async fn handle_callback(
|
||||
}
|
||||
|
||||
if payment_order.status == PaymentStatus::Paid {
|
||||
return Ok(());
|
||||
return Ok(payment_order);
|
||||
}
|
||||
|
||||
if payment_order.status != PaymentStatus::Pending {
|
||||
@@ -567,7 +968,7 @@ pub async fn handle_callback(
|
||||
}
|
||||
|
||||
if callback.status != PaymentStatus::Paid {
|
||||
return Ok(());
|
||||
return Ok(payment_order);
|
||||
}
|
||||
|
||||
if let Some(ref provider_tx_id) = callback.provider_tx_id
|
||||
@@ -576,7 +977,7 @@ pub async fn handle_callback(
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Ok(());
|
||||
return Ok(payment_order);
|
||||
}
|
||||
|
||||
crate::in_transaction!(pool, tx, {
|
||||
@@ -594,7 +995,6 @@ pub async fn handle_callback(
|
||||
"callback for order {} skipped: CAS failed (already processed)",
|
||||
payment_order.document_id
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(ref provider_tx_id) = callback.provider_tx_id {
|
||||
@@ -667,7 +1067,7 @@ pub async fn handle_callback(
|
||||
None
|
||||
);
|
||||
|
||||
Ok(())
|
||||
Ok(payment_order)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
@@ -123,9 +123,7 @@ impl PostServiceImpl {
|
||||
auth: &AuthUser,
|
||||
req: CreatePostRequest,
|
||||
) -> AppResult<(CreatePostRequest, crate::aspects::Dispatched)> {
|
||||
self.aspect_engine
|
||||
.before_create(self.repo.pool(), "posts", auth, req)
|
||||
.await
|
||||
self.aspect_engine.before_create("posts", auth, req).await
|
||||
}
|
||||
|
||||
async fn before_update(
|
||||
@@ -136,7 +134,7 @@ impl PostServiceImpl {
|
||||
) -> AppResult<(UpdatePostRequest, crate::aspects::Dispatched)> {
|
||||
crate::policy::PostPolicy::can_update(auth, existing)?;
|
||||
self.aspect_engine
|
||||
.before_update(self.repo.pool(), "posts", auth, existing, req)
|
||||
.before_update("posts", auth, existing, req)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -147,7 +145,7 @@ impl PostServiceImpl {
|
||||
) -> AppResult<crate::aspects::Dispatched> {
|
||||
crate::policy::PostPolicy::can_delete(auth, existing)?;
|
||||
self.aspect_engine
|
||||
.before_delete(self.repo.pool(), "posts", auth, existing)
|
||||
.before_delete("posts", auth, existing)
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
+669
-576
File diff suppressed because it is too large
Load Diff
+177
-106
@@ -1,77 +1,159 @@
|
||||
//! Tag service.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::aspects::engine::AspectEngine;
|
||||
use crate::dto::CreateTagRequest;
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::event::Event;
|
||||
use crate::middleware::auth::AuthUser;
|
||||
use crate::models::tag::Tag;
|
||||
use crate::repositories::TagRepository;
|
||||
|
||||
pub fn generate_slug(name: &str) -> String {
|
||||
crate::aspects::slug_aspect::generate_slug(name)
|
||||
}
|
||||
|
||||
pub async fn create_tag(
|
||||
tag_repo: &dyn TagRepository,
|
||||
auth: &AuthUser,
|
||||
req: CreateTagRequest,
|
||||
slug: String,
|
||||
) -> AppResult<crate::models::tag::Tag> {
|
||||
tag_repo
|
||||
.create(&req.name, &slug, auth.tenant_id(), auth.user_int_id())
|
||||
.await
|
||||
#[async_trait]
|
||||
pub trait TagService: Send + Sync {
|
||||
async fn create(&self, auth: &AuthUser, req: CreateTagRequest) -> AppResult<Tag>;
|
||||
async fn update(&self, auth: &AuthUser, id: &str, name: String, slug: String)
|
||||
-> AppResult<Tag>;
|
||||
async fn delete(&self, id: &str, auth: &AuthUser) -> AppResult<()>;
|
||||
async fn get(&self, id: &str, auth: &AuthUser) -> AppResult<Tag>;
|
||||
async fn list(&self, auth: &AuthUser) -> AppResult<Vec<Tag>>;
|
||||
async fn list_paginated(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<Tag>, i64)>;
|
||||
}
|
||||
|
||||
pub async fn delete_tag(tag_repo: &dyn TagRepository, id: &str, auth: &AuthUser) -> AppResult<()> {
|
||||
let tag = tag_repo
|
||||
.find_by_document_id(id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("tag"))?;
|
||||
tag_repo.delete(tag.id, auth.tenant_id()).await?;
|
||||
Ok(())
|
||||
pub struct TagServiceImpl {
|
||||
repo: Arc<dyn TagRepository>,
|
||||
aspect_engine: Arc<AspectEngine>,
|
||||
}
|
||||
|
||||
pub async fn get_tag(
|
||||
tag_repo: &dyn TagRepository,
|
||||
id: &str,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<crate::models::tag::Tag> {
|
||||
tag_repo
|
||||
.find_by_document_id(id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("tag"))
|
||||
impl TagServiceImpl {
|
||||
pub fn new(repo: Arc<dyn TagRepository>, aspect_engine: Arc<AspectEngine>) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
aspect_engine,
|
||||
}
|
||||
}
|
||||
|
||||
async fn before_create(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
req: CreateTagRequest,
|
||||
) -> AppResult<(CreateTagRequest, crate::aspects::Dispatched)> {
|
||||
self.aspect_engine.before_create("tags", auth, req).await
|
||||
}
|
||||
|
||||
async fn before_update(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
existing: &Tag,
|
||||
req: (String, String),
|
||||
) -> AppResult<((String, String), crate::aspects::Dispatched)> {
|
||||
self.aspect_engine
|
||||
.before_update("tags", auth, existing, req)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn before_delete(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
existing: &Tag,
|
||||
) -> AppResult<crate::aspects::Dispatched> {
|
||||
self.aspect_engine
|
||||
.before_delete("tags", auth, existing)
|
||||
.await
|
||||
}
|
||||
|
||||
fn after_created(&self, tag: &Tag) {
|
||||
self.aspect_engine.emit(Event::TagCreated(tag.clone()));
|
||||
}
|
||||
|
||||
fn after_updated(&self, tag: &Tag) {
|
||||
self.aspect_engine.emit(Event::TagUpdated(tag.clone()));
|
||||
}
|
||||
|
||||
fn after_deleted(&self, tag: &Tag) {
|
||||
self.aspect_engine.emit(Event::TagDeleted(tag.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_tag(
|
||||
tag_repo: &dyn TagRepository,
|
||||
id: &str,
|
||||
auth: &AuthUser,
|
||||
name: String,
|
||||
slug: String,
|
||||
) -> AppResult<crate::models::tag::Tag> {
|
||||
let tag = tag_repo
|
||||
.find_by_document_id(id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("tag"))?;
|
||||
tag_repo
|
||||
.update(tag.id, &name, &slug, auth.tenant_id())
|
||||
.await
|
||||
}
|
||||
#[async_trait]
|
||||
impl TagService for TagServiceImpl {
|
||||
async fn create(&self, auth: &AuthUser, req: CreateTagRequest) -> AppResult<Tag> {
|
||||
let (req, _d) = self.before_create(auth, req).await?;
|
||||
let slug = generate_slug(&req.name);
|
||||
let tag = self
|
||||
.repo
|
||||
.create(&req.name, &slug, auth.tenant_id(), auth.user_int_id())
|
||||
.await?;
|
||||
self.after_created(&tag);
|
||||
Ok(tag)
|
||||
}
|
||||
|
||||
pub async fn list_tags(
|
||||
tag_repo: &dyn TagRepository,
|
||||
auth: &AuthUser,
|
||||
) -> AppResult<Vec<crate::models::tag::Tag>> {
|
||||
tag_repo.find_all(auth.tenant_id()).await
|
||||
}
|
||||
async fn update(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
id: &str,
|
||||
name: String,
|
||||
slug: String,
|
||||
) -> AppResult<Tag> {
|
||||
let tag = self
|
||||
.repo
|
||||
.find_by_document_id(id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("tag"))?;
|
||||
let ((name, slug), _d) = self.before_update(auth, &tag, (name, slug)).await?;
|
||||
let updated = self
|
||||
.repo
|
||||
.update(tag.id, &name, &slug, auth.tenant_id())
|
||||
.await?;
|
||||
self.after_updated(&updated);
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
pub async fn list_tags_paginated(
|
||||
tag_repo: &dyn TagRepository,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<crate::models::tag::Tag>, i64)> {
|
||||
tag_repo
|
||||
.find_paginated(auth.tenant_id(), page, page_size)
|
||||
.await
|
||||
async fn delete(&self, id: &str, auth: &AuthUser) -> AppResult<()> {
|
||||
let tag = self
|
||||
.repo
|
||||
.find_by_document_id(id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("tag"))?;
|
||||
self.before_delete(auth, &tag).await?;
|
||||
self.repo.delete(tag.id, auth.tenant_id()).await?;
|
||||
self.after_deleted(&tag);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get(&self, id: &str, auth: &AuthUser) -> AppResult<Tag> {
|
||||
self.repo
|
||||
.find_by_document_id(id, auth.tenant_id())
|
||||
.await?
|
||||
.ok_or_else(|| AppError::not_found("tag"))
|
||||
}
|
||||
|
||||
async fn list(&self, auth: &AuthUser) -> AppResult<Vec<Tag>> {
|
||||
self.repo.find_all(auth.tenant_id()).await
|
||||
}
|
||||
|
||||
async fn list_paginated(
|
||||
&self,
|
||||
auth: &AuthUser,
|
||||
page: i64,
|
||||
page_size: i64,
|
||||
) -> AppResult<(Vec<Tag>, i64)> {
|
||||
self.repo
|
||||
.find_paginated(auth.tenant_id(), page, page_size)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -98,21 +180,27 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn make_service(pool: crate::db::Pool) -> Arc<dyn TagService> {
|
||||
Arc::new(TagServiceImpl::new(
|
||||
Arc::new(SqlxTagRepository::new(pool.clone())),
|
||||
Arc::new(AspectEngine::new()),
|
||||
))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_tag_basic() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxTagRepository::new(pool.clone());
|
||||
let svc = make_service(pool.clone());
|
||||
let a = auth();
|
||||
let tag = super::create_tag(
|
||||
&repo,
|
||||
&a,
|
||||
CreateTagRequest {
|
||||
name: "Rust".into(),
|
||||
},
|
||||
super::generate_slug("Rust"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let tag = svc
|
||||
.create(
|
||||
&a,
|
||||
CreateTagRequest {
|
||||
name: "Rust".into(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(tag.name, "Rust");
|
||||
assert_eq!(tag.slug, "rust");
|
||||
}
|
||||
@@ -120,34 +208,25 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn list_tags_empty() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxTagRepository::new(pool.clone());
|
||||
let svc = make_service(pool.clone());
|
||||
let a = auth();
|
||||
let tags = super::list_tags(&repo, &a).await.unwrap();
|
||||
let tags = svc.list(&a).await.unwrap();
|
||||
assert!(tags.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_tag() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxTagRepository::new(pool.clone());
|
||||
let svc = make_service(pool.clone());
|
||||
let a = auth();
|
||||
let tag = super::create_tag(
|
||||
&repo,
|
||||
&a,
|
||||
CreateTagRequest { name: "Old".into() },
|
||||
super::generate_slug("Old"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let updated = super::update_tag(
|
||||
&repo,
|
||||
&tag.document_id,
|
||||
&a,
|
||||
"New".into(),
|
||||
super::generate_slug("New"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let tag = svc
|
||||
.create(&a, CreateTagRequest { name: "Old".into() })
|
||||
.await
|
||||
.unwrap();
|
||||
let updated = svc
|
||||
.update(&a, &tag.document_id, "New".into(), generate_slug("New"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(updated.name, "New");
|
||||
assert_eq!(updated.slug, "new");
|
||||
}
|
||||
@@ -155,38 +234,32 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn delete_tag() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxTagRepository::new(pool.clone());
|
||||
let svc = make_service(pool.clone());
|
||||
let a = auth();
|
||||
let tag = super::create_tag(
|
||||
&repo,
|
||||
&a,
|
||||
CreateTagRequest { name: "Del".into() },
|
||||
super::generate_slug("Del"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
super::delete_tag(&repo, &tag.document_id, &a)
|
||||
let tag = svc
|
||||
.create(&a, CreateTagRequest { name: "Del".into() })
|
||||
.await
|
||||
.unwrap();
|
||||
let tags = super::list_tags(&repo, &a).await.unwrap();
|
||||
svc.delete(&tag.document_id, &a).await.unwrap();
|
||||
let tags = svc.list(&a).await.unwrap();
|
||||
assert!(tags.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_tag_not_found() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxTagRepository::new(pool.clone());
|
||||
let svc = make_service(pool.clone());
|
||||
let a = auth();
|
||||
assert!(super::delete_tag(&repo, "no-such-tag", &a).await.is_err());
|
||||
assert!(svc.delete("no-such-tag", &a).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_tag_not_found() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxTagRepository::new(pool.clone());
|
||||
let svc = make_service(pool.clone());
|
||||
let a = auth();
|
||||
assert!(
|
||||
super::update_tag(&repo, "missing", &a, "X".into(), super::generate_slug("X"))
|
||||
svc.update(&a, "missing", "X".into(), generate_slug("X"))
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
@@ -195,21 +268,19 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn list_tags_paginated() {
|
||||
let pool = setup_pool().await;
|
||||
let repo = SqlxTagRepository::new(pool.clone());
|
||||
let svc = make_service(pool.clone());
|
||||
let a = auth();
|
||||
for i in 0..5 {
|
||||
super::create_tag(
|
||||
&repo,
|
||||
svc.create(
|
||||
&a,
|
||||
CreateTagRequest {
|
||||
name: format!("Tag{i}"),
|
||||
},
|
||||
super::generate_slug(&format!("Tag{i}")),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
let (tags, total) = super::list_tags_paginated(&repo, &a, 1, 3).await.unwrap();
|
||||
let (tags, total) = svc.list_paginated(&a, 1, 3).await.unwrap();
|
||||
assert_eq!(total, 5);
|
||||
assert_eq!(tags.len(), 3);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::aspects::engine::AspectEngine;
|
||||
use crate::db::dialect::ph;
|
||||
use crate::db::pool::DbConnection;
|
||||
use crate::errors::app_error::{AppError, AppResult};
|
||||
use crate::eventbus::Event;
|
||||
use crate::models::currencies;
|
||||
use crate::models::wallet;
|
||||
use crate::models::wallet::WalletStatus;
|
||||
@@ -8,6 +14,92 @@ use crate::models::wallet_transaction::WalletTransaction;
|
||||
use crate::models::wallet_transaction::{WalletEntryType, WalletReferenceType, WalletTxType};
|
||||
use crate::repositories::WalletRepository;
|
||||
|
||||
#[async_trait]
|
||||
pub trait WalletService: Send + Sync {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn credit(
|
||||
&self,
|
||||
user_id: i64,
|
||||
currency: &str,
|
||||
amount: i64,
|
||||
tx_type: WalletTxType,
|
||||
transaction_no: &str,
|
||||
reference_type: Option<WalletReferenceType>,
|
||||
reference_id: Option<&str>,
|
||||
metadata: Option<&str>,
|
||||
) -> AppResult<WalletTransaction>;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn debit(
|
||||
&self,
|
||||
user_id: i64,
|
||||
currency: &str,
|
||||
amount: i64,
|
||||
tx_type: WalletTxType,
|
||||
transaction_no: &str,
|
||||
reference_type: Option<WalletReferenceType>,
|
||||
reference_id: Option<&str>,
|
||||
metadata: Option<&str>,
|
||||
) -> AppResult<WalletTransaction>;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn transfer(
|
||||
&self,
|
||||
from_user_id: i64,
|
||||
to_user_id: i64,
|
||||
currency: &str,
|
||||
amount: i64,
|
||||
transaction_no: &str,
|
||||
reference_type: Option<WalletReferenceType>,
|
||||
reference_id: Option<&str>,
|
||||
metadata: Option<&str>,
|
||||
) -> AppResult<(WalletTransaction, WalletTransaction)>;
|
||||
|
||||
async fn reverse_transaction(
|
||||
&self,
|
||||
original_tx_id: i64,
|
||||
transaction_no: &str,
|
||||
) -> AppResult<WalletTransaction>;
|
||||
|
||||
async fn tx_to_response(
|
||||
&self,
|
||||
tx: WalletTransaction,
|
||||
) -> AppResult<crate::dto::WalletTransactionResponse>;
|
||||
|
||||
async fn tx_list_to_response(
|
||||
&self,
|
||||
rows: Vec<WalletTransaction>,
|
||||
) -> AppResult<Vec<crate::dto::WalletTransactionResponse>>;
|
||||
}
|
||||
|
||||
pub struct WalletServiceImpl {
|
||||
repo: Arc<dyn WalletRepository>,
|
||||
aspect_engine: Arc<AspectEngine>,
|
||||
pool: Arc<crate::db::Pool>,
|
||||
}
|
||||
|
||||
impl WalletServiceImpl {
|
||||
pub fn new(
|
||||
repo: Arc<dyn WalletRepository>,
|
||||
aspect_engine: Arc<AspectEngine>,
|
||||
pool: Arc<crate::db::Pool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
repo,
|
||||
aspect_engine,
|
||||
pool,
|
||||
}
|
||||
}
|
||||
|
||||
fn after_credited(&self, tx: &WalletTransaction) {
|
||||
self.aspect_engine.emit(Event::WalletCredited(tx.clone()));
|
||||
}
|
||||
|
||||
fn after_debited(&self, tx: &WalletTransaction) {
|
||||
self.aspect_engine.emit(Event::WalletDebited(tx.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_currency_active(tx: &mut DbConnection, currency: &str) -> AppResult<()> {
|
||||
currencies::find_by_code_tx(tx, currency)
|
||||
.await?
|
||||
@@ -652,6 +744,131 @@ pub async fn tx_list_to_response(
|
||||
Ok(responses)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WalletService for WalletServiceImpl {
|
||||
async fn credit(
|
||||
&self,
|
||||
user_id: i64,
|
||||
currency: &str,
|
||||
amount: i64,
|
||||
tx_type: WalletTxType,
|
||||
transaction_no: &str,
|
||||
reference_type: Option<WalletReferenceType>,
|
||||
reference_id: Option<&str>,
|
||||
metadata: Option<&str>,
|
||||
) -> AppResult<WalletTransaction> {
|
||||
let tx = credit_wallet(
|
||||
self.repo.as_ref(),
|
||||
&self.pool,
|
||||
user_id,
|
||||
currency,
|
||||
amount,
|
||||
tx_type,
|
||||
transaction_no,
|
||||
reference_type,
|
||||
reference_id,
|
||||
metadata,
|
||||
)
|
||||
.await?;
|
||||
self.after_credited(&tx);
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
async fn debit(
|
||||
&self,
|
||||
user_id: i64,
|
||||
currency: &str,
|
||||
amount: i64,
|
||||
tx_type: WalletTxType,
|
||||
transaction_no: &str,
|
||||
reference_type: Option<WalletReferenceType>,
|
||||
reference_id: Option<&str>,
|
||||
metadata: Option<&str>,
|
||||
) -> AppResult<WalletTransaction> {
|
||||
let tx = debit_wallet(
|
||||
self.repo.as_ref(),
|
||||
&self.pool,
|
||||
user_id,
|
||||
currency,
|
||||
amount,
|
||||
tx_type,
|
||||
transaction_no,
|
||||
reference_type,
|
||||
reference_id,
|
||||
metadata,
|
||||
)
|
||||
.await?;
|
||||
self.after_debited(&tx);
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
async fn transfer(
|
||||
&self,
|
||||
from_user_id: i64,
|
||||
to_user_id: i64,
|
||||
currency: &str,
|
||||
amount: i64,
|
||||
transaction_no: &str,
|
||||
reference_type: Option<WalletReferenceType>,
|
||||
reference_id: Option<&str>,
|
||||
metadata: Option<&str>,
|
||||
) -> AppResult<(WalletTransaction, WalletTransaction)> {
|
||||
let (out_tx, in_tx) = transfer(
|
||||
self.repo.as_ref(),
|
||||
&self.pool,
|
||||
from_user_id,
|
||||
to_user_id,
|
||||
currency,
|
||||
amount,
|
||||
transaction_no,
|
||||
reference_type,
|
||||
reference_id,
|
||||
metadata,
|
||||
)
|
||||
.await?;
|
||||
self.after_debited(&out_tx);
|
||||
self.after_credited(&in_tx);
|
||||
Ok((out_tx, in_tx))
|
||||
}
|
||||
|
||||
async fn reverse_transaction(
|
||||
&self,
|
||||
original_tx_id: i64,
|
||||
transaction_no: &str,
|
||||
) -> AppResult<WalletTransaction> {
|
||||
let tx = reverse_transaction(
|
||||
self.repo.as_ref(),
|
||||
&self.pool,
|
||||
original_tx_id,
|
||||
transaction_no,
|
||||
)
|
||||
.await?;
|
||||
match tx.entry_type {
|
||||
crate::models::wallet_transaction::WalletEntryType::Credit => {
|
||||
self.after_credited(&tx);
|
||||
}
|
||||
crate::models::wallet_transaction::WalletEntryType::Debit => {
|
||||
self.after_debited(&tx);
|
||||
}
|
||||
}
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
async fn tx_to_response(
|
||||
&self,
|
||||
tx: WalletTransaction,
|
||||
) -> AppResult<crate::dto::WalletTransactionResponse> {
|
||||
tx_to_response(self.repo.as_ref(), tx).await
|
||||
}
|
||||
|
||||
async fn tx_list_to_response(
|
||||
&self,
|
||||
rows: Vec<WalletTransaction>,
|
||||
) -> AppResult<Vec<crate::dto::WalletTransactionResponse>> {
|
||||
tx_list_to_response(self.repo.as_ref(), rows).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -121,21 +121,65 @@ async fn build_test_app(pool: raisfast::db::Pool) -> (axum::Router, AppState) {
|
||||
Arc::new(NoopSearchEngine),
|
||||
))
|
||||
},
|
||||
page_service: Arc::new(raisfast::services::page::PageServiceImpl::new(
|
||||
Arc::new(pool.clone()),
|
||||
Arc::new(raisfast::aspects::engine::AspectEngine::new()),
|
||||
)),
|
||||
user_repo: Arc::new(SqlxUserRepository::new(pool.clone())),
|
||||
category_repo: Arc::new(SqlxCategoryRepository::new(pool.clone())),
|
||||
category_service: Arc::new(raisfast::services::category::CategoryServiceImpl::new(
|
||||
Arc::new(SqlxCategoryRepository::new(pool.clone())),
|
||||
Arc::new(raisfast::aspects::engine::AspectEngine::new()),
|
||||
)),
|
||||
tag_repo: Arc::new(SqlxTagRepository::new(pool.clone())),
|
||||
tag_service: Arc::new(raisfast::services::tag::TagServiceImpl::new(
|
||||
Arc::new(SqlxTagRepository::new(pool.clone())),
|
||||
Arc::new(raisfast::aspects::engine::AspectEngine::new()),
|
||||
)),
|
||||
comment_repo: Arc::new(SqlxCommentRepository::new(pool.clone())),
|
||||
comment_service: Arc::new(raisfast::services::comment::CommentServiceImpl::new(
|
||||
Arc::new(CachedPostRepository::new(
|
||||
SqlxPostRepository::new(pool.clone()),
|
||||
Arc::new(MemoryCache::new()),
|
||||
None,
|
||||
)),
|
||||
Arc::new(SqlxCommentRepository::new(pool.clone())),
|
||||
Arc::new(raisfast::aspects::engine::AspectEngine::new()),
|
||||
)),
|
||||
media_repo: Arc::new(SqlxMediaRepository::new(pool.clone())),
|
||||
refresh_token_repo: Arc::new(SqlxRefreshTokenRepository::new(pool.clone())),
|
||||
wallet_repo: Arc::new(raisfast::repositories::SqlxWalletRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
wallet_service: Arc::new(raisfast::services::wallet::WalletServiceImpl::new(
|
||||
Arc::new(raisfast::repositories::SqlxWalletRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
Arc::new(raisfast::aspects::engine::AspectEngine::new()),
|
||||
Arc::new(pool.clone()),
|
||||
)),
|
||||
product_repo: Arc::new(raisfast::repositories::SqlxProductRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
product_service: Arc::new(raisfast::services::product::ProductServiceImpl::new(
|
||||
Arc::new(raisfast::repositories::SqlxProductRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
Arc::new(raisfast::aspects::engine::AspectEngine::new()),
|
||||
)),
|
||||
order_repo: Arc::new(raisfast::repositories::SqlxOrderRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
order_service: Arc::new(raisfast::services::order::OrderServiceImpl::new(
|
||||
Arc::new(raisfast::repositories::SqlxOrderRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
Arc::new(raisfast::repositories::SqlxProductRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
Arc::new(raisfast::aspects::engine::AspectEngine::new()),
|
||||
Arc::new(pool.clone()),
|
||||
)),
|
||||
payment_channel_repo: Arc::new(raisfast::repositories::SqlxPaymentChannelRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
@@ -148,6 +192,30 @@ async fn build_test_app(pool: raisfast::db::Pool) -> (axum::Router, AppState) {
|
||||
payment_refund_repo: Arc::new(raisfast::repositories::SqlxPaymentRefundRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
payment_service: Arc::new(raisfast::services::payment::PaymentServiceImpl::new(
|
||||
Arc::new(raisfast::repositories::SqlxPaymentChannelRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
Arc::new(raisfast::repositories::SqlxPaymentOrderRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
Arc::new(raisfast::repositories::SqlxPaymentTransactionRepository::new(pool.clone())),
|
||||
Arc::new(raisfast::repositories::SqlxPaymentRefundRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
Arc::new(raisfast::repositories::SqlxOrderRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
Arc::new(raisfast::repositories::SqlxProductRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
Arc::new(raisfast::repositories::SqlxWalletRepository::new(
|
||||
pool.clone(),
|
||||
)),
|
||||
config.clone(),
|
||||
Arc::new(raisfast::aspects::engine::AspectEngine::new()),
|
||||
Arc::new(pool.clone()),
|
||||
)),
|
||||
search: Arc::new(NoopSearchEngine),
|
||||
content_type_registry: Arc::new(raisfast::content_type::ContentTypeRegistry::new()),
|
||||
aspect_engine: {
|
||||
|
||||
+18
-28
@@ -68,14 +68,14 @@ async fn setup_pool() -> sqlx::SqlitePool {
|
||||
pool
|
||||
}
|
||||
async fn create_test_user(pool: &sqlx::SqlitePool, label: &str) -> (i64, String) {
|
||||
let eventbus = raisfast::eventbus::EventBus::new(16);
|
||||
let aspect_engine = raisfast::aspects::engine::AspectEngine::new();
|
||||
let user_repo = SqlxUserRepository::new(pool.clone());
|
||||
let req = raisfast::dto::RegisterRequest {
|
||||
username: format!("user_{label}"),
|
||||
email: format!("{label}@test.com"),
|
||||
password: "Password123".into(),
|
||||
};
|
||||
let user = auth::register(&user_repo, &eventbus, req, None, false, pool)
|
||||
let user = auth::register(&user_repo, &aspect_engine, req, None, false, pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let row: (i64,) = sqlx::query_as("SELECT id FROM users WHERE document_id = ?")
|
||||
@@ -125,7 +125,7 @@ label = "优先级"
|
||||
async fn tauri_auth_register_service() {
|
||||
let pool = setup_pool().await;
|
||||
let user_repo = SqlxUserRepository::new(pool.clone());
|
||||
let eventbus = raisfast::eventbus::EventBus::new(16);
|
||||
let aspect_engine = raisfast::aspects::engine::AspectEngine::new();
|
||||
|
||||
let req = raisfast::dto::RegisterRequest {
|
||||
username: "testuser".into(),
|
||||
@@ -133,7 +133,7 @@ async fn tauri_auth_register_service() {
|
||||
password: "Password123".into(),
|
||||
};
|
||||
|
||||
let result = auth::register(&user_repo, &eventbus, req, None, false, &pool).await;
|
||||
let result = auth::register(&user_repo, &aspect_engine, req, None, false, &pool).await;
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
@@ -148,14 +148,14 @@ async fn tauri_auth_register_service() {
|
||||
async fn tauri_auth_register_duplicate_email() {
|
||||
let pool = setup_pool().await;
|
||||
let user_repo = SqlxUserRepository::new(pool.clone());
|
||||
let eventbus = raisfast::eventbus::EventBus::new(16);
|
||||
let aspect_engine = raisfast::aspects::engine::AspectEngine::new();
|
||||
|
||||
let req = raisfast::dto::RegisterRequest {
|
||||
username: "user1".into(),
|
||||
email: "dup@example.com".into(),
|
||||
password: "Password123".into(),
|
||||
};
|
||||
auth::register(&user_repo, &eventbus, req, None, false, &pool)
|
||||
auth::register(&user_repo, &aspect_engine, req, None, false, &pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -164,8 +164,8 @@ async fn tauri_auth_register_duplicate_email() {
|
||||
email: "dup@example.com".into(),
|
||||
password: "Password456".into(),
|
||||
};
|
||||
let eventbus2 = raisfast::eventbus::EventBus::new(16);
|
||||
let result = auth::register(&user_repo, &eventbus2, req2, None, false, &pool).await;
|
||||
let aspect_engine2 = raisfast::aspects::engine::AspectEngine::new();
|
||||
let result = auth::register(&user_repo, &aspect_engine2, req2, None, false, &pool).await;
|
||||
assert!(result.is_err(), "duplicate email should fail");
|
||||
}
|
||||
|
||||
@@ -175,15 +175,14 @@ async fn tauri_auth_login_service() {
|
||||
let config = test_config();
|
||||
let user_repo = SqlxUserRepository::new(pool.clone());
|
||||
let refresh_repo = SqlxRefreshTokenRepository::new(pool.clone());
|
||||
let eventbus = raisfast::eventbus::EventBus::new(16);
|
||||
let plugin_mgr = raisfast::plugins::PluginManager::new(Arc::new(config.clone())).await;
|
||||
let aspect_engine = raisfast::aspects::engine::AspectEngine::new();
|
||||
|
||||
let reg_req = raisfast::dto::RegisterRequest {
|
||||
username: "loginuser".into(),
|
||||
email: "login@example.com".into(),
|
||||
password: "Password123".into(),
|
||||
};
|
||||
auth::register(&user_repo, &eventbus, reg_req, None, false, &pool)
|
||||
auth::register(&user_repo, &aspect_engine, reg_req, None, false, &pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -191,12 +190,11 @@ async fn tauri_auth_login_service() {
|
||||
email: "login@example.com".into(),
|
||||
password: "Password123".into(),
|
||||
};
|
||||
let eventbus2 = raisfast::eventbus::EventBus::new(16);
|
||||
let aspect_engine2 = raisfast::aspects::engine::AspectEngine::new();
|
||||
let result = auth::login(
|
||||
&user_repo,
|
||||
&refresh_repo,
|
||||
&plugin_mgr,
|
||||
&eventbus2,
|
||||
&aspect_engine2,
|
||||
&pool,
|
||||
&login_req,
|
||||
&config.jwt_secret,
|
||||
@@ -219,15 +217,14 @@ async fn tauri_auth_login_wrong_password() {
|
||||
let config = test_config();
|
||||
let user_repo = SqlxUserRepository::new(pool.clone());
|
||||
let refresh_repo = SqlxRefreshTokenRepository::new(pool.clone());
|
||||
let eventbus = raisfast::eventbus::EventBus::new(16);
|
||||
let plugin_mgr = raisfast::plugins::PluginManager::new(Arc::new(config.clone())).await;
|
||||
let aspect_engine = raisfast::aspects::engine::AspectEngine::new();
|
||||
|
||||
let reg_req = raisfast::dto::RegisterRequest {
|
||||
username: "wrongpw".into(),
|
||||
email: "wrong@example.com".into(),
|
||||
password: "Password123".into(),
|
||||
};
|
||||
auth::register(&user_repo, &eventbus, reg_req, None, false, &pool)
|
||||
auth::register(&user_repo, &aspect_engine, reg_req, None, false, &pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -235,12 +232,11 @@ async fn tauri_auth_login_wrong_password() {
|
||||
email: "wrong@example.com".into(),
|
||||
password: "WrongPassword".into(),
|
||||
};
|
||||
let eventbus2 = raisfast::eventbus::EventBus::new(16);
|
||||
let aspect_engine2 = raisfast::aspects::engine::AspectEngine::new();
|
||||
let result = auth::login(
|
||||
&user_repo,
|
||||
&refresh_repo,
|
||||
&plugin_mgr,
|
||||
&eventbus2,
|
||||
&aspect_engine2,
|
||||
&pool,
|
||||
&login_req,
|
||||
&config.jwt_secret,
|
||||
@@ -258,14 +254,14 @@ async fn tauri_auth_login_wrong_password() {
|
||||
async fn tauri_auth_get_me_service() {
|
||||
let pool = setup_pool().await;
|
||||
let user_repo = SqlxUserRepository::new(pool.clone());
|
||||
let eventbus = raisfast::eventbus::EventBus::new(16);
|
||||
let aspect_engine = raisfast::aspects::engine::AspectEngine::new();
|
||||
|
||||
let reg_req = raisfast::dto::RegisterRequest {
|
||||
username: "getme".into(),
|
||||
email: "getme@example.com".into(),
|
||||
password: "Password123".into(),
|
||||
};
|
||||
let user = auth::register(&user_repo, &eventbus, reg_req, None, false, &pool)
|
||||
let user = auth::register(&user_repo, &aspect_engine, reg_req, None, false, &pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -295,9 +291,6 @@ async fn tauri_post_create_and_list() {
|
||||
Arc::new(raisfast::cache::MemoryCache::new()),
|
||||
None,
|
||||
));
|
||||
let eventbus = raisfast::eventbus::EventBus::new(16);
|
||||
let config = test_config();
|
||||
let plugin_mgr = raisfast::plugins::PluginManager::new(Arc::new(config.clone())).await;
|
||||
let svc = build_post_service(post_repo.clone());
|
||||
|
||||
let req = raisfast::dto::CreatePostRequest {
|
||||
@@ -338,9 +331,6 @@ async fn tauri_post_get_by_slug() {
|
||||
Arc::new(raisfast::cache::MemoryCache::new()),
|
||||
None,
|
||||
));
|
||||
let eventbus = raisfast::eventbus::EventBus::new(16);
|
||||
let config = test_config();
|
||||
let plugin_mgr = raisfast::plugins::PluginManager::new(Arc::new(config.clone())).await;
|
||||
let svc = build_post_service(post_repo.clone());
|
||||
|
||||
let req = raisfast::dto::CreatePostRequest {
|
||||
|
||||
Reference in New Issue
Block a user