diff --git a/.gitignore b/.gitignore index d19c2815..cf5b9970 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,22 @@ + +# Rust +/backend/target/ +/sdk/rust/target/ +**/*.rs.bk +Cargo.lock + +# Node.js +/frontend/node_modules/ +/frontend/.next/ +/frontend/out/ +/frontend/build/ +.env +.env.local + +# IDEs +.vscode/ +.idea/ + # Rust build artifacts backend/target/ **/target/ @@ -25,10 +44,14 @@ frontend/.env.local *.swo *~ + # OS .DS_Store Thumbs.db + +# Others + # Logs *.log npm-debug.log* @@ -41,6 +64,7 @@ coverage/ # Temporary files smart-contract/contracts/src/simple_test.rs + smart-contract/contracts/test_snapshots/ issue.md checks.md diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 66ae041e..f381e91e 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -22,6 +22,8 @@ thiserror = "1.0" anyhow = "1.0" config = "0.14" async-trait = "0.1" +reqwest = { version = "0.11", features = ["json"] } +tokio-retry = "0.3" # Stellar/Soroban dependencies soroban-sdk = "21.0" # OpenAPI documentation @@ -30,5 +32,4 @@ utoipa-swagger-ui = { version = "6.0", features = ["axum"] } [dev-dependencies] tower-test = "0.4" -reqwest = { version = "0.11", features = ["json"] } testcontainers = "0.15" diff --git a/backend/migrations/20240103000000_add_resilience_tables.sql b/backend/migrations/20240103000000_add_resilience_tables.sql new file mode 100644 index 00000000..83cbf079 --- /dev/null +++ b/backend/migrations/20240103000000_add_resilience_tables.sql @@ -0,0 +1,47 @@ +"""-- Create Disruption Predictions Table +CREATE TABLE disruption_predictions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + product_id VARCHAR(255) NOT NULL, + predicted_at TIMESTAMPTZ NOT NULL, + probability FLOAT NOT NULL, + impact_level VARCHAR(50) NOT NULL, + details JSONB NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Create Supplier Risks Table +CREATE TABLE supplier_risks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + supplier_name VARCHAR(255) NOT NULL, + risk_score FLOAT NOT NULL, + risk_factors JSONB NOT NULL, + last_assessed_at TIMESTAMPTZ NOT NULL +); + +-- Create Geographic Risks Table +CREATE TABLE geographic_risks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + location VARCHAR(255) NOT NULL, + risk_score FLOAT NOT NULL, + risk_factors JSONB NOT NULL, + last_assessed_at TIMESTAMPTZ NOT NULL +); + +-- Create Alternative Sources Table +CREATE TABLE alternative_sources ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + product_id VARCHAR(255) NOT NULL, + alternative_supplier VARCHAR(255) NOT NULL, + viability_score FLOAT NOT NULL, + details JSONB NOT NULL +); + +-- Create Inventory Recommendations Table +CREATE TABLE inventory_recommendations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + product_id VARCHAR(255) NOT NULL, + recommended_safety_stock INT NOT NULL, + rationale TEXT NOT NULL, + generated_at TIMESTAMPTZ NOT NULL +); +""" \ No newline at end of file diff --git a/backend/src/main.rs b/backend/src/main.rs index 69a665cb..9deb8a59 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -12,6 +12,7 @@ mod services; mod models; mod database; mod utils; +mod resilience; mod error; mod docs; mod blockchain; @@ -20,7 +21,11 @@ mod compliance; use config::Config; use database::Database; + +use services::{ProductService, EventService, UserService, ApiKeyService, SyncService, FinancialService, AnalyticsService, ResilienceService}; + use services::{ProductService, EventService, UserService, ApiKeyService, SyncService, FinancialService, AnalyticsService, CarbonService}; + use utils::CronService; use error::AppError; @@ -34,6 +39,8 @@ pub struct AppState { pub sync_service: Arc, pub financial_service: Arc, pub analytics_service: Arc, + Chain-Resilience + pub resilience_service: Arc, pub carbon_service: Arc, pub config: Config, } @@ -59,6 +66,8 @@ impl AppState { db.pool().clone(), config.redis.url.clone(), )); + + let resilience_service = Arc::new(ResilienceService::new(db.pool().clone())); let carbon_service = Arc::new(CarbonService::new(db.pool().clone())); Ok(Self { @@ -70,7 +79,11 @@ impl AppState { sync_service, financial_service, analytics_service, + + resilience_service, + carbon_service, + config, }) } @@ -94,6 +107,7 @@ async fn main() -> Result<(), Box> { let app = Router::new() .merge(crate::routes::health_routes()) .merge(crate::routes::api_routes()) + .merge(crate::routes::resilience::resilience_routes()) .merge(crate::docs::create_swagger_ui()) .layer( ServiceBuilder::new() diff --git a/backend/src/models/mod.rs b/backend/src/models/mod.rs new file mode 100644 index 00000000..9a96993b --- /dev/null +++ b/backend/src/models/mod.rs @@ -0,0 +1,3 @@ +pub mod analytics; +pub mod resilience; +pub mod product; \ No newline at end of file diff --git a/backend/src/models/product.rs b/backend/src/models/product.rs new file mode 100644 index 00000000..4b2b961e --- /dev/null +++ b/backend/src/models/product.rs @@ -0,0 +1,11 @@ +"""use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] +pub struct Product { + pub id: String, + pub name: String, + pub latitude: f64, + pub longitude: f64, + pub country_code: String, +} +""" \ No newline at end of file diff --git a/backend/src/models/resilience.rs b/backend/src/models/resilience.rs new file mode 100644 index 00000000..4aca09df --- /dev/null +++ b/backend/src/models/resilience.rs @@ -0,0 +1,60 @@ +"""use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] +pub struct DisruptionPrediction { + pub id: Uuid, + pub product_id: String, + pub predicted_at: DateTime, + pub probability: f64, + pub impact_level: String, + pub details: serde_json::Value, + pub created_at: DateTime, +} + +#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] +pub struct SupplierRisk { + pub id: Uuid, + pub supplier_name: String, + pub risk_score: f64, + pub risk_factors: serde_json::Value, + pub last_assessed_at: DateTime, +} + +#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] +pub struct GeographicRisk { + pub id: Uuid, + pub location: String, + pub risk_score: f64, + pub risk_factors: serde_json::Value, + pub last_assessed_at: DateTime, +} + +#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] +pub struct AlternativeSource { + pub id: Uuid, + pub product_id: String, + pub alternative_supplier: String, + pub viability_score: f64, + pub details: serde_json::Value, +} + +#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)] +pub struct InventoryRecommendation { + pub id: Uuid, + pub product_id: String, + pub recommended_safety_stock: i32, + pub rationale: String, + pub generated_at: DateTime, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ResilienceMetrics { + pub disruption_predictions: Vec, + pub supplier_risks: Vec, + pub geographic_risks: Vec, + pub alternative_sources: Vec, + pub inventory_recommendations: Vec, +} +"" \ No newline at end of file diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs new file mode 100644 index 00000000..b5017fa4 --- /dev/null +++ b/backend/src/routes/mod.rs @@ -0,0 +1,2 @@ +pub mod analytics; +pub mod resilience; \ No newline at end of file diff --git a/backend/src/routes/resilience.rs b/backend/src/routes/resilience.rs new file mode 100644 index 00000000..6a09cfa7 --- /dev/null +++ b/backend/src/routes/resilience.rs @@ -0,0 +1,24 @@ +"""use axum::{ + extract::{State, Path}, + routing::get, + Json, + Router, +}; +use std::sync::Arc; + +use crate::error::AppError; +use crate::models::resilience::ResilienceMetrics; +use crate::AppState; + +pub fn resilience_routes() -> Router { + Router::new().route("/resilience/:product_id", get(get_resilience_metrics)) +} + +async fn get_resilience_metrics( + State(state): State, + Path(product_id): Path, +) -> Result, AppError> { + let metrics = state.resilience_service.get_resilience_metrics(&product_id).await?; + Ok(Json(metrics)) +} +""" \ No newline at end of file diff --git a/backend/src/services/mod.rs b/backend/src/services/mod.rs new file mode 100644 index 00000000..c1a7275a --- /dev/null +++ b/backend/src/services/mod.rs @@ -0,0 +1,2 @@ +pub mod analytics_service; +pub mod resilience_service; \ No newline at end of file diff --git a/backend/src/services/resilience_service.rs b/backend/src/services/resilience_service.rs new file mode 100644 index 00000000..2ee52335 --- /dev/null +++ b/backend/src/services/resilience_service.rs @@ -0,0 +1,138 @@ +"""use sqlx::PgPool; + +use crate::error::AppError; +use crate::models::resilience::*; +use crate::models::product::Product; +use reqwest::Client; +use serde_json::Value; +use tokio_retry::Retry; +use tokio_retry::strategy::{ExponentialBackoff, jitter}; + +pub struct ResilienceService { + pool: PgPool, + http_client: Client, +} + +impl ResilienceService { + pub fn new(pool: PgPool) -> Self { + Self { pool, http_client: Client::new() } + } + + pub async fn get_resilience_metrics(&self, product_id: &str) -> Result { + let product = self.get_product_details(product_id).await?; + + let news_data = self.get_news_data(&product.name).await?; + let weather_data = self.get_weather_data(product.latitude, product.longitude).await?; + let political_risk_data = self.get_political_risk_data(&product.country_code).await?; + + let predictions = self.generate_disruption_predictions(&news_data, &weather_data, &political_risk_data).await?; + let suppliers = self.assess_supplier_risks(&product).await?; + let locations = self.assess_geographic_risks(&product, &weather_data, &political_risk_data).await?; + let alternatives = self.identify_alternative_sources(&product).await?; + let inventory = self.recommend_inventory_levels(&product, &predictions).await?; + + Ok(ResilienceMetrics { + disruption_predictions: predictions, + supplier_risks: suppliers, + geographic_risks: locations, + alternative_sources: alternatives, + inventory_recommendations: inventory, + }) + } + + async fn get_disruption_predictions(&self, product_id: &str) -> Result, AppError> { + let rows = sqlx::query_as!(DisruptionPrediction, "SELECT * FROM disruption_predictions WHERE product_id = $1", product_id) + .fetch_all(&self.pool) + .await?; + Ok(rows) + } + + async fn get_supplier_risks(&self, _product_id: &str) -> Result, AppError> { + let rows = sqlx::query_as!(SupplierRisk, "SELECT * FROM supplier_risks") + .fetch_all(&self.pool) + .await?; + Ok(rows) + } + + async fn get_geographic_risks(&self, _product_id: &str) -> Result, AppError> { + let rows = sqlx::query_as!(GeographicRisk, "SELECT * FROM geographic_risks") + .fetch_all(&self.pool) + .await?; + Ok(rows) + } + + async fn get_alternative_sources(&self, product_id: &str) -> Result, AppError> { + let rows = sqlx::query_as!(AlternativeSource, "SELECT * FROM alternative_sources WHERE product_id = $1", product_id) + .fetch_all(&self.pool) + .await?; + Ok(rows) + } + + async fn get_inventory_recommendations(&self, product_id: &str) -> Result, AppError> { + let rows = sqlx::query_as!(InventoryRecommendation, "SELECT * FROM inventory_recommendations WHERE product_id = $1", product_id) + .fetch_all(&self.pool) + .await?; + Ok(rows) + } + + async fn get_external_data(&self, url: &str) -> Result { + let retry_strategy = ExponentialBackoff::from_millis(100) + .map(jitter) + .take(3); + + let response = Retry::spawn(retry_strategy, || async { + self.http_client.get(url).send().await + }) + .await?; + + let json = response.json::().await?; + Ok(json) + } + + async fn get_news_data(&self, product_name: &str) -> Result { + let url = format!("https://newsapi.ai/api/v1/article/getArticles?query={}&apiKey={}", product_name, "YOUR_NEWSAPI_KEY"); + self.get_external_data(&url).await + } + + async fn get_product_details(&self, product_id: &str) -> Result { + let product = sqlx::query_as!(Product, "SELECT * FROM products WHERE id = $1", product_id) + .fetch_one(&self.pool) + .await?; + Ok(product) + } + + async fn generate_disruption_predictions(&self, _news_data: &Value, _weather_data: &Value, _political_risk_data: &Value) -> Result, AppError> { + // Placeholder implementation + Ok(vec![]) + } + + async fn recommend_inventory_levels(&self, _product: &Product, _predictions: &[DisruptionPrediction]) -> Result, AppError> { + // Placeholder implementation + Ok(vec![]) + } + + async fn identify_alternative_sources(&self, _product: &Product) -> Result, AppError> { + // Placeholder implementation + Ok(vec![]) + } + + async fn assess_geographic_risks(&self, _product: &Product, _weather_data: &Value, _political_risk_data: &Value) -> Result, AppError> { + // Placeholder implementation + Ok(vec![]) + } + + async fn assess_supplier_risks(&self, _product: &Product) -> Result, AppError> { + // Placeholder implementation + Ok(vec![]) + } + + async fn get_political_risk_data(&self, country_code: &str) -> Result { + let url = format!("https://api.prsgroup.com/v2/country/{}?api_key={}", country_code, "YOUR_PRSGROUP_KEY"); + self.get_external_data(&url).await + } + + async fn get_weather_data(&self, latitude: f64, longitude: f64) -> Result { + let url = format!("https://api.open-meteo.com/v1/forecast?latitude={}&longitude={}&hourly=temperature_2m,relativehumidity_2m,precipitation,windspeed_10m", latitude, longitude); + self.get_external_data(&url).await + } +""" \ No newline at end of file diff --git a/frontend/app/(app)/resilience/page.tsx b/frontend/app/(app)/resilience/page.tsx new file mode 100644 index 00000000..15f473cd --- /dev/null +++ b/frontend/app/(app)/resilience/page.tsx @@ -0,0 +1,42 @@ +"""'use client'; + +import { useEffect, useState } from 'react'; +import { ResilienceMetrics } from '@/lib/resilience'; +import { DisruptionPredictions } from '@/components/resilience/DisruptionPredictions'; +import { SupplierRisks } from '@/components/resilience/SupplierRisks'; +import { GeographicRisks } from '@/components/resilience/GeographicRisks'; +import { AlternativeSources } from '@/components/resilience/AlternativeSources'; +import { InventoryRecommendations } from '@/components/resilience/InventoryRecommendations'; + +export default function ResiliencePage() { + const [metrics, setMetrics] = useState(null); + const [productId, setProductId] = useState('some-product-id'); // Replace with actual product ID + + useEffect(() => { + async function fetchMetrics() { + const res = await fetch(`/api/resilience/${productId}`); + const data = await res.json(); + setMetrics(data); + } + + fetchMetrics(); + }, [productId]); + + if (!metrics) { + return
Loading...
; + } + + return ( +
+

Supply Chain Resilience

+
+ + + + + +
+
+ ); +} +""" \ No newline at end of file diff --git a/frontend/src/components/resilience/AlternativeSources.tsx b/frontend/src/components/resilience/AlternativeSources.tsx new file mode 100644 index 00000000..d4bc981a --- /dev/null +++ b/frontend/src/components/resilience/AlternativeSources.tsx @@ -0,0 +1,26 @@ +"""import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { AlternativeSource } from "@/lib/resilience"; + +interface AlternativeSourcesProps { + sources: AlternativeSource[]; +} + +export function AlternativeSources({ sources }: AlternativeSourcesProps) { + return ( + + + Alternative Sources + + + {sources.map((source) => ( +
+

Product: {source.product_id}

+

Alternative Supplier: {source.alternative_supplier}

+

Viability Score: {source.viability_score}

+
+ ))} +
+
+ ); +} +""" \ No newline at end of file diff --git a/frontend/src/components/resilience/DisruptionPredictions.tsx b/frontend/src/components/resilience/DisruptionPredictions.tsx new file mode 100644 index 00000000..b08ddc5f --- /dev/null +++ b/frontend/src/components/resilience/DisruptionPredictions.tsx @@ -0,0 +1,26 @@ +"""import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { DisruptionPrediction } from "@/lib/resilience"; + +interface DisruptionPredictionsProps { + predictions: DisruptionPrediction[]; +} + +export function DisruptionPredictions({ predictions }: DisruptionPredictionsProps) { + return ( + + + Disruption Predictions + + + {predictions.map((prediction) => ( +
+

Product: {prediction.product_id}

+

Probability: {prediction.probability}

+

Impact: {prediction.impact_level}

+
+ ))} +
+
+ ); +} +""" \ No newline at end of file diff --git a/frontend/src/components/resilience/GeographicRisks.tsx b/frontend/src/components/resilience/GeographicRisks.tsx new file mode 100644 index 00000000..962bf14a --- /dev/null +++ b/frontend/src/components/resilience/GeographicRisks.tsx @@ -0,0 +1,25 @@ +"""import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { GeographicRisk } from "@/lib/resilience"; + +interface GeographicRisksProps { + risks: GeographicRisk[]; +} + +export function GeographicRisks({ risks }: GeographicRisksProps) { + return ( + + + Geographic Risks + + + {risks.map((risk) => ( +
+

Location: {risk.location}

+

Risk Score: {risk.risk_score}

+
+ ))} +
+
+ ); +} +""" \ No newline at end of file diff --git a/frontend/src/components/resilience/InventoryRecommendations.tsx b/frontend/src/components/resilience/InventoryRecommendations.tsx new file mode 100644 index 00000000..e6d6873e --- /dev/null +++ b/frontend/src/components/resilience/InventoryRecommendations.tsx @@ -0,0 +1,26 @@ +"""import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { InventoryRecommendation } from "@/lib/resilience"; + +interface InventoryRecommendationsProps { + recommendations: InventoryRecommendation[]; +} + +export function InventoryRecommendations({ recommendations }: InventoryRecommendationsProps) { + return ( + + + Inventory Recommendations + + + {recommendations.map((rec) => ( +
+

Product: {rec.product_id}

+

Recommended Safety Stock: {rec.recommended_safety_stock}

+

Rationale: {rec.rationale}

+
+ ))} +
+
+ ); +} +""" \ No newline at end of file diff --git a/frontend/src/components/resilience/SupplierRisks.tsx b/frontend/src/components/resilience/SupplierRisks.tsx new file mode 100644 index 00000000..8ef91985 --- /dev/null +++ b/frontend/src/components/resilience/SupplierRisks.tsx @@ -0,0 +1,25 @@ +"""import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { SupplierRisk } from "@/lib/resilience"; + +interface SupplierRisksProps { + risks: SupplierRisk[]; +} + +export function SupplierRisks({ risks }: SupplierRisksProps) { + return ( + + + Supplier Risks + + + {risks.map((risk) => ( +
+

Supplier: {risk.supplier_name}

+

Risk Score: {risk.risk_score}

+
+ ))} +
+
+ ); +} +""" \ No newline at end of file diff --git a/frontend/src/lib/resilience.ts b/frontend/src/lib/resilience.ts new file mode 100644 index 00000000..6c98eb54 --- /dev/null +++ b/frontend/src/lib/resilience.ts @@ -0,0 +1,50 @@ +"""export interface DisruptionPrediction { + id: string; + product_id: string; + predicted_at: string; + probability: number; + impact_level: string; + details: any; + created_at: string; +} + +export interface SupplierRisk { + id: string; + supplier_name: string; + risk_score: number; + risk_factors: any; + last_assessed_at: string; +} + +export interface GeographicRisk { + id: string; + location: string; + risk_score: number; + risk_factors: any; + last_assessed_at: string; +} + +export interface AlternativeSource { + id: string; + product_id: string; + alternative_supplier: string; + viability_score: number; + details: any; +} + +export interface InventoryRecommendation { + id: string; + product_id: string; + recommended_safety_stock: number; + rationale: string; + generated_at: string; +} + +export interface ResilienceMetrics { + disruption_predictions: DisruptionPrediction[]; + supplier_risks: SupplierRisk[]; + geographic_risks: GeographicRisk[]; + alternative_sources: AlternativeSource[]; + inventory_recommendations: InventoryRecommendation[]; +} +""" \ No newline at end of file