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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions backend/migrations/004_add_distributions.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- Adds a distributions table which stores each distributed badge item.
-- Each row is one delivered badge and belongs to a batch (distribution_id).

CREATE TABLE IF NOT EXISTS distributions (
id UUID PRIMARY KEY,
distribution_id UUID NOT NULL,
address TEXT NOT NULL,
badge_name TEXT NOT NULL,
metadata JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_distributions_distribution_id ON distributions(distribution_id);

CREATE INDEX IF NOT EXISTS idx_distributions_address ON distributions(address);
37 changes: 37 additions & 0 deletions backend/src/application/commands/create_distribution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// src/application/commands/create_distribution.rs
use uuid::Uuid;
use anyhow::Result;
use crate::domain::repositories::distribution_repository::DistributionRepository;
use crate::domain::entities::distribution::Distribution;
use crate::domain::value_objects::WalletAddress;
use serde_json::Value;

pub struct CreateDistribution<'a> {
pub repo: &'a dyn DistributionRepository,
}

impl<'a> CreateDistribution<'a> {
pub fn new(repo: &'a dyn DistributionRepository) -> Self {
Self { repo }
}

pub async fn execute(
&self,
items: Vec<(String, String, Option<Value>)>,
_batch_metadata: Option<Value>,
) -> Result<Uuid> {
let distribution_id = Uuid::new_v4();
let mut domain_items = Vec::with_capacity(items.len());

for (address_str, badge_name, metadata) in items {
let wallet_address = WalletAddress::new(address_str)
.map_err(|e| anyhow::anyhow!(e))?;
let item = Distribution::new(distribution_id, wallet_address, badge_name, metadata);
domain_items.push(item);
}

self.repo.insert_batch(distribution_id, domain_items).await?;

Ok(distribution_id)
}
}
1 change: 1 addition & 0 deletions backend/src/application/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod create_profile;
pub mod update_profile;
pub mod create_distribution;
26 changes: 26 additions & 0 deletions backend/src/application/dtos/distribution_dtos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use uuid::Uuid;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateDistributionRequest {
pub items: Vec<CreateDistributionDto>,
pub metadata: Option<Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateDistributionDto {
pub address: String,
pub badge_name: String,
pub metadata: Option<Value>,
}

#[derive(Debug, Serialize)]
pub struct DistributionResponse {
pub id: Uuid,
pub distribution_id: Uuid,
pub address: String,
pub badge_name: String,
pub metadata: serde_json::Value,
pub created_at: chrono::NaiveDateTime,
}
2 changes: 2 additions & 0 deletions backend/src/application/dtos/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub mod auth_dtos;
pub mod profile_dtos;
pub mod distribution_dtos;

pub use auth_dtos::*;
pub use profile_dtos::*;
pub use distribution_dtos::*;
28 changes: 28 additions & 0 deletions backend/src/domain/entities/distribution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use serde_json::Value;
use crate::domain::value_objects::WalletAddress;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Distribution {
pub id: Uuid,
pub distribution_id: Uuid,
pub address: WalletAddress,
pub badge_name: String,
pub metadata: Option<Value>,
pub created_at: DateTime<Utc>,
}

impl Distribution {
pub fn new(distribution_id: Uuid, address: WalletAddress, badge_name: String, metadata: Option<Value>) -> Self {
Self {
id: Uuid::new_v4(),
distribution_id,
address,
badge_name,
metadata,
created_at: Utc::now(),
}
}
}
2 changes: 2 additions & 0 deletions backend/src/domain/entities/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod profile;
pub mod distribution;

pub use profile::Profile;
pub use distribution::Distribution;
13 changes: 13 additions & 0 deletions backend/src/domain/repositories/distribution_repository.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use async_trait::async_trait;
use anyhow::Result;
use uuid::Uuid;
use crate::domain::entities::distribution::Distribution;

#[async_trait]
pub trait DistributionRepository: Send + Sync {
async fn insert_batch(&self, distribution_id: Uuid, items: Vec<Distribution>) -> Result<()>;

async fn get_by_distribution_id(&self, distribution_id: Uuid) -> Result<Vec<Distribution>>;

async fn get_by_address(&self, address: &str) -> Result<Vec<Distribution>>;
}
2 changes: 2 additions & 0 deletions backend/src/domain/repositories/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod profile_repository;
pub mod distribution_repository;

pub use profile_repository::ProfileRepository;
pub use distribution_repository::DistributionRepository;
2 changes: 2 additions & 0 deletions backend/src/infrastructure/repositories/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
pub mod postgres_profile_repository;
pub mod postgres_distribution_repository;

pub use postgres_profile_repository::PostgresProfileRepository;
pub use postgres_distribution_repository::PostgresDistributionRepository;
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
use crate::domain::entities::distribution::Distribution;
use crate::domain::repositories::distribution_repository::DistributionRepository;
use async_trait::async_trait;
use anyhow::Result;
use sqlx::PgPool;
use uuid::Uuid;
use crate::domain::value_objects::WalletAddress;

pub struct PostgresDistributionRepository {
pool: PgPool,
}

impl PostgresDistributionRepository {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
}

#[async_trait]
impl DistributionRepository for PostgresDistributionRepository {
async fn insert_batch(
&self,
distribution_id: Uuid,
items: Vec<Distribution>,
) -> Result<(), anyhow::Error> {
for item in items {
let metadata_json = item.metadata.clone();

sqlx::query!(
r#"
INSERT INTO distributions (id, distribution_id, address, badge_name, metadata, created_at)
VALUES ($1, $2, $3, $4, $5::jsonb, $6)
"#,
item.id,
distribution_id,
item.address.as_str(),
item.badge_name,
metadata_json,
item.created_at
)
.execute(&self.pool)
.await
.map_err(|e| anyhow::Error::new(e))?;
}

Ok(())
}

async fn get_by_distribution_id(&self, distribution_id: Uuid) -> Result<Vec<Distribution>> {
let rows = sqlx::query!(
r#"
SELECT id, distribution_id, address, badge_name, metadata, created_at
FROM distributions
WHERE distribution_id = $1
ORDER BY created_at ASC
"#,
distribution_id
)
.fetch_all(&self.pool)
.await?;

let items = rows
.into_iter()
.map(|r| {
Distribution {
id: r.id,
distribution_id: r.distribution_id,
address: WalletAddress::new(r.address).expect("Invalid wallet address"),
badge_name: r.badge_name,
metadata: r.metadata,
created_at: r.created_at.expect("created_at should not be null"),
}
})
.collect();

Ok(items)
}

async fn get_by_address(&self, address: &str) -> Result<Vec<Distribution>> {
let rows = sqlx::query!(
r#"
SELECT id, distribution_id, address, badge_name, metadata, created_at
FROM distributions
WHERE address = $1
ORDER BY created_at DESC
"#,
address
)
.fetch_all(&self.pool)
.await?;

let items = rows
.into_iter()
.map(|r| {
Distribution {
id: r.id,
distribution_id: r.distribution_id,
address: WalletAddress::new(r.address).expect("Invalid wallet address"),
badge_name: r.badge_name,
metadata: r.metadata,
created_at: r.created_at.expect("created_at should not be null"),
}
})
.collect();

Ok(items)
}
}
Loading
Loading