diff --git a/Cargo.lock b/Cargo.lock index 96879f41..f7d08db4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3147,6 +3147,7 @@ dependencies = [ "openubl-common", "openubl-entity", "openubl-migration", + "openubl-storage", "sea-orm", "sea-query", "serde", @@ -3177,7 +3178,6 @@ name = "openubl-entity" version = "0.1.0" dependencies = [ "sea-orm", - "serde", ] [[package]] diff --git a/openubl/api/Cargo.toml b/openubl/api/Cargo.toml index 11fc59af..f7ad7861 100644 --- a/openubl/api/Cargo.toml +++ b/openubl/api/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" openubl-entity = {path = "../entity"} openubl-common = {path = "../common"} openubl-migration = {path = "../migration"} +openubl-storage = { path = "../storage" } xsender = {path = "../../xsender"} diff --git a/openubl/api/src/system/project.rs b/openubl/api/src/system/project.rs index 24464323..fe7d8166 100644 --- a/openubl/api/src/system/project.rs +++ b/openubl/api/src/system/project.rs @@ -120,6 +120,22 @@ impl ProjectContext { } // Documents + pub async fn get_document( + &self, + id: i32, + tx: Transactional<'_>, + ) -> Result, Error> { + Ok(entity::ubl_document::Entity::find() + .join( + JoinType::InnerJoin, + entity::credentials::Relation::Project.def(), + ) + .filter(entity::credentials::Column::Id.eq(id)) + .filter(entity::credentials::Column::ProjectId.eq(self.project.id)) + .one(&self.system.connection(tx)) + .await? + .map(|e| (&self.system, e).into())) + } pub async fn get_document_by_ubl_params( &self, @@ -205,9 +221,31 @@ impl ProjectContext { .map(|e| (&self.system, e).into())) } + pub async fn get_credential_for_supplier_id( + &self, + supplier_id: &str, + tx: Transactional<'_>, + ) -> Result, Error> { + Ok(entity::credentials::Entity::find() + .join( + JoinType::InnerJoin, + entity::credentials::Relation::Project.def(), + ) + .join( + JoinType::InnerJoin, + entity::credentials::Relation::SendRule.def(), + ) + .filter(entity::send_rule::Column::SupplierId.eq(supplier_id)) + .filter(entity::credentials::Column::ProjectId.eq(self.project.id)) + .one(&self.system.connection(tx)) + .await? + .map(|e| (&self.system, e).into())) + } + pub async fn create_credentials( &self, model: &entity::credentials::Model, + supplier_ids: &[String], tx: Transactional<'_>, ) -> Result { let entity = entity::credentials::ActiveModel { @@ -221,6 +259,20 @@ impl ProjectContext { }; let result = entity.insert(&self.system.connection(tx)).await?; + + let rules = supplier_ids + .iter() + .map(|supplier_id| entity::send_rule::ActiveModel { + supplier_id: Set(supplier_id.clone()), + credentials_id: Set(result.id), + project_id: Set(self.project.id), + ..Default::default() + }) + .collect::>(); + let _rules = entity::send_rule::Entity::insert_many(rules) + .exec(&self.system.connection(tx)) + .await?; + Ok((&self.system, result).into()) } diff --git a/openubl/api/src/system/ubl_document.rs b/openubl/api/src/system/ubl_document.rs index 016ca8dd..8d2147ac 100644 --- a/openubl/api/src/system/ubl_document.rs +++ b/openubl/api/src/system/ubl_document.rs @@ -16,18 +16,18 @@ impl From<(&InnerSystem, entity::ubl_document::Model)> for UblDocumentContext { } } -// impl UblDocumentContext { -// fn send_to_sunat(credentials: &entity::credentials::Model) { -// FileSender { -// urls: Urls { -// invoice: credentials.url_invoice.clone(), -// perception_retention: credentials.url_perception_retention.clone(), -// despatch: credentials.url_despatch.clone(), -// }, -// credentials: Credentials { -// username: credentials.username_sol.clone(), -// password: credentials.password_sol.clone(), -// }, -// }; -// } -// } +impl UblDocumentContext { + // fn send_to_sunat(credentials: &entity::credentials::Model) { + // FileSender { + // urls: Urls { + // invoice: credentials.url_invoice.clone(), + // perception_retention: credentials.url_perception_retention.clone(), + // despatch: credentials.url_despatch.clone(), + // }, + // credentials: Credentials { + // username: credentials.username_sol.clone(), + // password: credentials.password_sol.clone(), + // }, + // }; + // } +} diff --git a/openubl/cli/src/main.rs b/openubl/cli/src/main.rs index 1a3a4d3c..28078a22 100644 --- a/openubl/cli/src/main.rs +++ b/openubl/cli/src/main.rs @@ -2,10 +2,13 @@ use std::process::{ExitCode, Termination}; use clap::Parser; +mod sender; + #[allow(clippy::large_enum_variant)] #[derive(clap::Subcommand, Debug)] pub enum Command { - Server(openubl_server::Run), + Server(openubl_server::ServerRun), + // Sender(SenderRun), } #[derive(clap::Parser, Debug)] @@ -41,6 +44,7 @@ impl Cli { async fn run_command(self) -> anyhow::Result { match self.command { Command::Server(run) => run.run().await, + // Command::Sender(run) => run.run().await } } } diff --git a/openubl/cli/src/sender.rs b/openubl/cli/src/sender.rs new file mode 100644 index 00000000..fb15f219 --- /dev/null +++ b/openubl/cli/src/sender.rs @@ -0,0 +1,11 @@ +#[derive(clap::Args, Debug)] +pub struct SenderRun { + #[arg(id = "input", long)] + pub input: Vec, +} + +impl SenderRun { + // pub async fn run(self) -> anyhow::Result { + // Ok(ExitCode::SUCCESS) + // } +} diff --git a/openubl/entity/Cargo.toml b/openubl/entity/Cargo.toml index 871dd5de..5d635c0c 100644 --- a/openubl/entity/Cargo.toml +++ b/openubl/entity/Cargo.toml @@ -5,5 +5,4 @@ edition = "2021" publish = false [dependencies] -sea-orm = { version = "0.12.10", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"] } -serde = { version = "1.0.193", features = ["derive"] } \ No newline at end of file +sea-orm = { version = "0.12.10", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"] } \ No newline at end of file diff --git a/openubl/entity/src/credentials.rs b/openubl/entity/src/credentials.rs index 8e44e55f..066f32ab 100644 --- a/openubl/entity/src/credentials.rs +++ b/openubl/entity/src/credentials.rs @@ -1,22 +1,20 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10 use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "credentials")] pub struct Model { - #[serde(skip_deserializing)] #[sea_orm(primary_key)] pub id: i32, pub name: String, - pub url_invoice: String, - pub url_despatch: String, - pub url_perception_retention: String, pub username_sol: String, pub password_sol: String, pub client_id: String, pub client_secret: String, + pub url_invoice: String, + pub url_despatch: String, + pub url_perception_retention: String, pub project_id: i32, } @@ -30,6 +28,8 @@ pub enum Relation { on_delete = "Cascade" )] Project, + #[sea_orm(has_many = "super::send_rule::Entity")] + SendRule, } impl Related for Entity { @@ -38,4 +38,10 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::SendRule.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/openubl/entity/src/lib.rs b/openubl/entity/src/lib.rs index 8fd080d8..3d715f0f 100644 --- a/openubl/entity/src/lib.rs +++ b/openubl/entity/src/lib.rs @@ -6,5 +6,6 @@ pub mod credentials; pub mod keystore; pub mod keystore_config; pub mod project; +pub mod send_rule; pub mod ubl_document; pub mod user_role; diff --git a/openubl/entity/src/mod.rs b/openubl/entity/src/mod.rs index 8fd080d8..3d715f0f 100644 --- a/openubl/entity/src/mod.rs +++ b/openubl/entity/src/mod.rs @@ -6,5 +6,6 @@ pub mod credentials; pub mod keystore; pub mod keystore_config; pub mod project; +pub mod send_rule; pub mod ubl_document; pub mod user_role; diff --git a/openubl/entity/src/prelude.rs b/openubl/entity/src/prelude.rs index 804ff0e4..3d86315f 100644 --- a/openubl/entity/src/prelude.rs +++ b/openubl/entity/src/prelude.rs @@ -4,5 +4,6 @@ pub use super::credentials::Entity as Credentials; pub use super::keystore::Entity as Keystore; pub use super::keystore_config::Entity as KeystoreConfig; pub use super::project::Entity as Project; +pub use super::send_rule::Entity as SendRule; pub use super::ubl_document::Entity as UblDocument; pub use super::user_role::Entity as UserRole; diff --git a/openubl/entity/src/project.rs b/openubl/entity/src/project.rs index 669a34d7..a8ee4c5d 100644 --- a/openubl/entity/src/project.rs +++ b/openubl/entity/src/project.rs @@ -1,12 +1,10 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10 use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "project")] pub struct Model { - #[serde(skip_deserializing)] #[sea_orm(primary_key)] pub id: i32, pub name: String, @@ -19,6 +17,8 @@ pub enum Relation { Credentials, #[sea_orm(has_many = "super::keystore::Entity")] Keystore, + #[sea_orm(has_many = "super::send_rule::Entity")] + SendRule, #[sea_orm(has_many = "super::ubl_document::Entity")] UblDocument, #[sea_orm(has_many = "super::user_role::Entity")] @@ -37,6 +37,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::SendRule.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::UblDocument.def() diff --git a/openubl/entity/src/send_rule.rs b/openubl/entity/src/send_rule.rs new file mode 100644 index 00000000..c8fa799f --- /dev/null +++ b/openubl/entity/src/send_rule.rs @@ -0,0 +1,47 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "send_rule")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub supplier_id: String, + pub credentials_id: i32, + pub project_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::credentials::Entity", + from = "Column::CredentialsId", + to = "super::credentials::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Credentials, + #[sea_orm( + belongs_to = "super::project::Entity", + from = "Column::ProjectId", + to = "super::project::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Project, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Credentials.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Project.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/openubl/entity/src/ubl_document.rs b/openubl/entity/src/ubl_document.rs index 96082955..cbc38430 100644 --- a/openubl/entity/src/ubl_document.rs +++ b/openubl/entity/src/ubl_document.rs @@ -1,12 +1,10 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10 use sea_orm::entity::prelude::*; -use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] #[sea_orm(table_name = "ubl_document")] pub struct Model { - #[serde(skip_deserializing)] #[sea_orm(primary_key)] pub id: i32, pub project_id: i32, diff --git a/openubl/migration/src/lib.rs b/openubl/migration/src/lib.rs index 5a363849..d9400082 100644 --- a/openubl/migration/src/lib.rs +++ b/openubl/migration/src/lib.rs @@ -6,6 +6,7 @@ mod m20240101_104121_create_ubl_document; mod m20240113_213636_create_keystore; mod m20240113_213657_create_keystore_config; mod m20240114_154538_create_credentials; +mod m20240117_142858_create_send_rule; pub struct Migrator; #[async_trait::async_trait] @@ -18,6 +19,7 @@ impl MigratorTrait for Migrator { Box::new(m20240113_213636_create_keystore::Migration), Box::new(m20240113_213657_create_keystore_config::Migration), Box::new(m20240114_154538_create_credentials::Migration), + Box::new(m20240117_142858_create_send_rule::Migration), ] } } diff --git a/openubl/migration/src/m20240114_154538_create_credentials.rs b/openubl/migration/src/m20240114_154538_create_credentials.rs index 07de58ab..5208d4d7 100644 --- a/openubl/migration/src/m20240114_154538_create_credentials.rs +++ b/openubl/migration/src/m20240114_154538_create_credentials.rs @@ -61,7 +61,7 @@ impl MigrationTrait for Migration { } #[derive(DeriveIden)] -enum Credentials { +pub enum Credentials { Table, Id, Name, diff --git a/openubl/migration/src/m20240117_142858_create_send_rule.rs b/openubl/migration/src/m20240117_142858_create_send_rule.rs new file mode 100644 index 00000000..d7e2c52b --- /dev/null +++ b/openubl/migration/src/m20240117_142858_create_send_rule.rs @@ -0,0 +1,63 @@ +use crate::m20231223_071007_create_project::Project; +use crate::m20240114_154538_create_credentials::Credentials; +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(SendRule::Table) + .if_not_exists() + .col( + ColumnDef::new(SendRule::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(SendRule::SupplierId).string().not_null()) + .col(ColumnDef::new(SendRule::CredentialsId).integer().not_null()) + .col(ColumnDef::new(SendRule::ProjectId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .from_col(SendRule::CredentialsId) + .to(Credentials::Table, Credentials::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .from_col(SendRule::ProjectId) + .to(Project::Table, Project::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .index( + Index::create() + .col(SendRule::SupplierId) + .col(SendRule::ProjectId) + .unique(), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(SendRule::Table).to_owned()) + .await + } +} + +#[derive(DeriveIden)] +enum SendRule { + Table, + Id, + SupplierId, + CredentialsId, + ProjectId, +} diff --git a/openubl/server/src/dto.rs b/openubl/server/src/dto.rs new file mode 100644 index 00000000..76835f4f --- /dev/null +++ b/openubl/server/src/dto.rs @@ -0,0 +1,105 @@ +use openubl_entity as entity; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct ProjectDto { + pub id: i32, + pub name: String, + pub description: Option, +} + +impl From for ProjectDto { + fn from(value: entity::project::Model) -> Self { + Self { + id: value.id, + name: value.name.clone(), + description: value.description.clone(), + } + } +} + +impl From for entity::project::Model { + fn from(value: ProjectDto) -> Self { + Self { + id: value.id, + name: value.name, + description: value.description, + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct UblDocumentDto { + pub id: i32, + pub supplier_id: String, + pub document_id: String, + pub document_type: String, + pub voided_document_code: Option, +} + +impl From for UblDocumentDto { + fn from(value: entity::ubl_document::Model) -> Self { + Self { + id: value.id, + supplier_id: value.supplier_id.clone(), + document_id: value.document_id.clone(), + document_type: value.document_type.clone(), + voided_document_code: value.voided_document_code.clone(), + } + } +} + +#[derive(Serialize, Deserialize)] +pub struct CredentialsDto { + pub id: i32, + pub name: String, + pub username_sol: String, + pub client_id: String, + pub url_invoice: String, + pub url_despatch: String, + pub url_perception_retention: String, +} + +#[derive(Serialize, Deserialize)] +pub struct NewCredentialsDto { + pub name: String, + pub username_sol: String, + pub password_sol: String, + pub client_id: String, + pub client_secret: String, + pub url_invoice: String, + pub url_despatch: String, + pub url_perception_retention: String, + pub supplier_ids_applied_to: Vec, +} + +impl From for CredentialsDto { + fn from(value: entity::credentials::Model) -> Self { + Self { + id: value.id, + name: value.name.clone(), + username_sol: value.username_sol.clone(), + client_id: value.client_id.clone(), + url_invoice: value.url_invoice.clone(), + url_despatch: value.url_despatch.clone(), + url_perception_retention: value.url_perception_retention.clone(), + } + } +} + +impl From for entity::credentials::Model { + fn from(value: NewCredentialsDto) -> Self { + Self { + id: 0, + name: value.name.clone(), + username_sol: value.username_sol.clone(), + password_sol: value.password_sol, + client_id: value.client_id.clone(), + client_secret: value.client_secret, + url_invoice: value.url_invoice.clone(), + url_despatch: value.url_despatch.clone(), + url_perception_retention: value.url_perception_retention.clone(), + project_id: 0, + } + } +} diff --git a/openubl/server/src/lib.rs b/openubl/server/src/lib.rs index 146e3b97..11cf4db9 100644 --- a/openubl/server/src/lib.rs +++ b/openubl/server/src/lib.rs @@ -14,11 +14,12 @@ use openubl_storage::StorageSystem; use crate::server::{files, health, project}; +mod dto; pub mod server; /// Run the API server #[derive(clap::Args, Debug)] -pub struct Run { +pub struct ServerRun { #[arg(short, long, env, default_value = "[::1]:8080")] pub bind_addr: String, @@ -37,7 +38,7 @@ pub struct Run { // pub search_engine: openubl_index::config::SearchEngine, } -impl Run { +impl ServerRun { pub async fn run(self) -> anyhow::Result { env_logger::init(); diff --git a/openubl/server/src/server/files.rs b/openubl/server/src/server/files.rs index 2befed5b..260e1d12 100644 --- a/openubl/server/src/server/files.rs +++ b/openubl/server/src/server/files.rs @@ -10,6 +10,7 @@ use openubl_entity::ubl_document; use openubl_oidc::UserClaims; use xsender::prelude::{FromPath, UblFile}; +use crate::dto::UblDocumentDto; use crate::server::Error; use crate::AppState; @@ -96,7 +97,7 @@ pub async fn upload_file( .create_document(&document_model, Transactional::None) .await?; - Ok(HttpResponse::Created().json(document_ctx.ubl_document)) + Ok(HttpResponse::Created().json(UblDocumentDto::from(document_ctx.ubl_document))) } Some(_) => Err(Error::BadRequest { status: StatusCode::CONFLICT, diff --git a/openubl/server/src/server/project.rs b/openubl/server/src/server/project.rs index d2e635e2..8ffb9412 100644 --- a/openubl/server/src/server/project.rs +++ b/openubl/server/src/server/project.rs @@ -7,6 +7,7 @@ use openubl_api::system::project::ProjectContext; use openubl_entity as entity; use openubl_oidc::UserClaims; +use crate::dto::{CredentialsDto, NewCredentialsDto, ProjectDto, UblDocumentDto}; use crate::server::Error; use crate::AppState; @@ -43,8 +44,8 @@ pub async fn list_projects( Ok(HttpResponse::Ok().json( projects_ctx - .iter() - .map(|ctx| &ctx.project) + .into_iter() + .map(|ctx| ProjectDto::from(ctx.project)) .collect::>(), )) } @@ -53,7 +54,7 @@ pub async fn list_projects( #[post("/projects")] pub async fn create_project( state: web::Data, - json: web::Json, + json: web::Json, user: AuthenticatedUser, ) -> Result { let prev = state @@ -64,14 +65,15 @@ pub async fn create_project( .iter() .any(|ctx| ctx.project.name == json.name); + let model = entity::project::Model::from(json.into_inner()); match prev { false => { let project_ctx = state .system - .create_project(&json, &user.claims.user_id(), Transactional::None) + .create_project(&model, &user.claims.user_id(), Transactional::None) .await .map_err(Error::System)?; - Ok(HttpResponse::Ok().json(project_ctx.project)) + Ok(HttpResponse::Ok().json(ProjectDto::from(project_ctx.project))) } true => Ok(HttpResponse::Conflict().body("Another project has the same name")), } @@ -87,7 +89,7 @@ pub async fn get_project( let project_id = path.into_inner(); let ctx = get_project_ctx(&state, project_id, &user, Transactional::None).await?; - Ok(HttpResponse::Ok().json(ctx.project)) + Ok(HttpResponse::Ok().json(ProjectDto::from(ctx.project))) } #[utoipa::path(responses((status = 204, description = "Update project")))] @@ -95,13 +97,14 @@ pub async fn get_project( pub async fn update_project( state: web::Data, path: web::Path, - json: web::Json, + json: web::Json, user: AuthenticatedUser, ) -> Result { let project_id = path.into_inner(); let ctx = get_project_ctx(&state, project_id, &user, Transactional::None).await?; - ctx.update(&json, Transactional::None).await?; + let model = entity::project::Model::from(json.into_inner()); + ctx.update(&model, Transactional::None).await?; Ok(HttpResponse::NoContent().finish()) } @@ -143,11 +146,87 @@ pub async fn list_documents( result .items .into_iter() - .map(|e| e.ubl_document) + .map(|e| UblDocumentDto::from(e.ubl_document)) .collect::>(), )) } +#[utoipa::path(responses((status = 200, description = "Get document's file")))] +#[get("/projects/{project_id}/documents/{document_id}/file")] +pub async fn get_document_file( + state: web::Data, + path: web::Path<(i32, i32)>, + user: AuthenticatedUser, +) -> Result { + let (project_id, document_id) = path.into_inner(); + let ctx = get_project_ctx(&state, project_id, &user, Transactional::None).await?; + + let document_ctx = ctx + .get_document(document_id, Transactional::None) + .await? + .ok_or(Error::BadRequest { + status: StatusCode::NOT_FOUND, + msg: "Document not found".to_string(), + })?; + + let xml_file = state + .storage + .download_ubl_xml(&document_ctx.ubl_document.file_id) + .await?; + + Ok(HttpResponse::Ok() + .append_header(("Content-Type", "application/xml")) + .body(xml_file)) +} + +// #[utoipa::path(responses((status = 200, description = "Get document's file")))] +// #[post("/projects/{project_id}/documents/{document_id}/send")] +// pub async fn send_document( +// state: web::Data, +// path: web::Path<(i32, i32)>, +// user: AuthenticatedUser, +// ) -> Result { +// let (project_id, document_id) = path.into_inner(); +// let ctx = get_project_ctx(&state, project_id, &user, Transactional::None).await?; +// +// let document_ctx = ctx +// .get_document(document_id, Transactional::None) +// .await? +// .ok_or(Error::BadRequest { +// status: StatusCode::NOT_FOUND, +// msg: "Document not found".to_string(), +// })?; +// +// let xml_file = state +// .storage +// .download_ubl_xml(&document_ctx.ubl_document.file_id) +// .await?; +// +// let credentials_ctx = ctx +// .get_credential_for_supplier_id("", Transactional::None) +// .await? +// .ok_or(Error::BadRequest { +// status: StatusCode::BAD_REQUEST, +// msg: "There is no credentials that match the supplier id of the document".to_string(), +// })?; +// +// // let a = FileSender { +// // urls: Urls { +// // invoice: "https://e-beta.sunat.gob.pe/ol-ti-itcpfegem-beta/billService".to_string(), +// // perception_retention:"https://e-beta.sunat.gob.pe/ol-ti-itemision-otroscpe-gem-beta/billService".to_string(), +// // despatch: "https://api-cpe.sunat.gob.pe/v1/contribuyente/gem".to_string(), +// // }, +// // credentials: Credentials { +// // username: "12345678959MODDATOS".to_string(), +// // password: "MODDATOS".to_string(), +// // }, +// // }; +// +// Ok(HttpResponse::Ok() +// .append_header(("Content-Type", "application/xml")) +// .body(xml_file)) +// } + // Credentials #[utoipa::path(responses((status = 200, description = "List credentials")))] @@ -165,7 +244,7 @@ pub async fn list_credentials( .await .map_err(Error::System)? .into_iter() - .map(|e| e.credentials) + .map(|e| CredentialsDto::from(e.credentials)) .collect::>(); Ok(HttpResponse::Ok().json(result)) @@ -176,17 +255,20 @@ pub async fn list_credentials( pub async fn create_credentials( state: web::Data, path: web::Path, - json: web::Json, + json: web::Json, user: AuthenticatedUser, ) -> Result { let project_id = path.into_inner(); let ctx = get_project_ctx(&state, project_id, &user, Transactional::None).await?; + let supplier_ids = json.supplier_ids_applied_to.clone(); + let model = entity::credentials::Model::from(json.into_inner()); + let credentials_ctx = ctx - .create_credentials(&json, Transactional::None) + .create_credentials(&model, &supplier_ids, Transactional::None) .await .map_err(Error::System)?; - Ok(HttpResponse::Ok().json(credentials_ctx.credentials)) + Ok(HttpResponse::Ok().json(CredentialsDto::from(credentials_ctx.credentials))) } #[utoipa::path(responses((status = 200, description = "Get credentials")))] @@ -207,7 +289,7 @@ pub async fn get_credentials( msg: "Project not found".to_string(), })?; - Ok(HttpResponse::Ok().json(credentials_ctx.credentials)) + Ok(HttpResponse::Ok().json(CredentialsDto::from(credentials_ctx.credentials))) } #[utoipa::path(responses((status = 204, description = "Update credentials")))] @@ -215,7 +297,7 @@ pub async fn get_credentials( pub async fn update_credentials( state: web::Data, path: web::Path<(i32, i32)>, - json: web::Json, + json: web::Json, user: AuthenticatedUser, ) -> Result { let (project_id, credentials_id) = path.into_inner(); @@ -229,7 +311,8 @@ pub async fn update_credentials( msg: "Project not found".to_string(), })?; - credentials_ctx.update(&json, Transactional::None).await?; + let model = entity::credentials::Model::from(json.into_inner()); + credentials_ctx.update(&model, Transactional::None).await?; Ok(HttpResponse::NoContent().finish()) } diff --git a/openubl/storage/src/lib.rs b/openubl/storage/src/lib.rs index 166ab945..692f95ef 100644 --- a/openubl/storage/src/lib.rs +++ b/openubl/storage/src/lib.rs @@ -1,6 +1,7 @@ +use anyhow::anyhow; use std::fs; -use std::fs::{rename, File}; -use std::io::{Read, Write}; +use std::fs::File; +use std::io::{Cursor, Read, Write}; use std::path::Path; use std::str::FromStr; @@ -13,14 +14,13 @@ use aws_sdk_s3::operation::get_object::GetObjectError; use aws_sdk_s3::operation::put_object::PutObjectError; use aws_sdk_s3::primitives::{ByteStream, ByteStreamError}; use aws_smithy_runtime_api::client::orchestrator::HttpResponse; -use minio::s3::args::UploadObjectArgs; +use minio::s3::args::{GetObjectArgs, PutObjectArgs}; use minio::s3::client::Client as MinioClient; use minio::s3::creds::StaticProvider; use minio::s3::http::BaseUrl; -use uuid::Uuid; use zip::result::{ZipError, ZipResult}; use zip::write::FileOptions; -use zip::ZipWriter; +use zip::{ZipArchive, ZipWriter}; use crate::config::Storage; @@ -147,7 +147,7 @@ impl StorageSystem { let credentials_provider = aws_sdk_s3::config::Credentials::new( &config.access_key, &config.secret_key, - Some("atestsessiontoken".to_string()), + Some("test_session_token".to_string()), None, "", ); @@ -169,6 +169,7 @@ impl StorageSystem { } } + /// Each file will be zipped before being uploaded to the Storage pub async fn upload_ubl_xml( &self, project_id: i32, @@ -178,11 +179,15 @@ impl StorageSystem { file_sha246: &str, file_full_path: &str, ) -> Result { + // Create zip let file_name_inside_zip = format!("{ruc}-{}.xml", document_id.to_uppercase()); - let zip_path = zip_file(file_full_path, &file_name_inside_zip)?; - let short_sha256: String = file_sha246.chars().take(7).collect(); - let zip_name = format!("{}_{short_sha256}.zip", document_id.to_uppercase()); + let zip_file = create_zip_from_path(file_full_path, &file_name_inside_zip)?; + let zip_file_name = format!( + "{}_{}.zip", + document_id.to_uppercase(), + file_sha246.chars().take(7).collect::() + ); match self { StorageSystem::Local(directories) => { @@ -190,26 +195,32 @@ impl StorageSystem { .join(project_id.to_string()) .join(ruc) .join(document_type) - .join(&zip_name); + .join(&zip_file_name); - rename(zip_path, object_name)?; - Ok(zip_name.clone()) + File::create(object_name)?.write_all(&zip_file)?; + Ok(zip_file_name.clone()) } StorageSystem::Minio(bucket, client) => { - let object_name = format!("{project_id}/{ruc}/{document_type}/{zip_name}"); + let object_name = format!("{project_id}/{ruc}/{document_type}/{zip_file_name}"); - let object = &UploadObjectArgs::new(&bucket.name, &object_name, &zip_path)?; - let response = client.upload_object(object).await?; + let object_stream_size = zip_file.len(); + let mut object_stream = Cursor::new(zip_file); - // Clear temp files - fs::remove_file(file_full_path)?; - fs::remove_file(zip_path)?; + let mut object = PutObjectArgs::new( + &bucket.name, + &object_name, + &mut object_stream, + Some(object_stream_size), + None, + )?; - Ok(response.object_name) + client.put_object(&mut object).await?; + + Ok(object_name) } StorageSystem::S3(buckets, client) => { - let object_name = format!("{project_id}/{ruc}/{document_type}/{zip_name}"); - let object = ByteStream::from_path(&zip_path).await?; + let object_name = format!("{project_id}/{ruc}/{document_type}/{zip_file_name}"); + let object = ByteStream::from(zip_file); client .put_object() @@ -219,46 +230,77 @@ impl StorageSystem { .send() .await?; - // Clear temp files - fs::remove_file(file_full_path)?; - fs::remove_file(zip_path)?; - Ok(object_name) } } } + + /// Each file will be unzipped after retrieved from storage + pub async fn download_ubl_xml(&self, file_id: &str) -> Result { + let zip_file = match self { + StorageSystem::Local(_) => fs::read(file_id)?, + StorageSystem::Minio(bucket, client) => { + let object = GetObjectArgs::new(&bucket.name, file_id)?; + + client.get_object(&object).await?.bytes().await?.to_vec() + } + StorageSystem::S3(buckets, client) => client + .get_object() + .bucket(&buckets.name) + .key(file_id) + .send() + .await? + .body + .try_next() + .await? + .ok_or(StorageSystemErr::Any(anyhow!("Could not find response")))? + .to_vec(), + }; + + let xml_file = extract_first_file_from_zip(&zip_file)?.ok_or(StorageSystemErr::Any( + anyhow!("Could not extract first file from zip"), + ))?; + + Ok(xml_file) + } +} + +pub fn create_zip_from_path(path: &str, file_name_inside_zip: &str) -> ZipResult> { + let file_content = fs::read_to_string(path)?; + create_zip_from_str(&file_content, file_name_inside_zip) } -pub fn zip_file( - full_path_of_file_to_be_zipped: &str, - file_name_to_be_used_in_zip: &str, -) -> ZipResult { - let zip_filename = format!("{}.zip", Uuid::new_v4()); +pub fn create_zip_from_str(content: &str, file_name_inside_zip: &str) -> ZipResult> { + let mut data = Vec::new(); - let mut file = File::open(full_path_of_file_to_be_zipped)?; - let file_path = Path::new(full_path_of_file_to_be_zipped); - let file_directory = file_path.parent().ok_or(ZipError::InvalidArchive( - "Could not find the parent folder of given file", - ))?; + { + let buff = Cursor::new(&mut data); + let mut zip = ZipWriter::new(buff); - let zip_path = file_directory.join(zip_filename); - let zip_file = File::create(zip_path.as_path())?; - let mut zip = ZipWriter::new(zip_file); + let file_options = FileOptions::default(); + zip.start_file(file_name_inside_zip, file_options)?; + zip.write_all(content.as_bytes())?; + zip.finish()?; + } - let file_options = FileOptions::default() - .compression_method(zip::CompressionMethod::Bzip2) - .unix_permissions(0o755); + Ok(data) +} - zip.start_file(file_name_to_be_used_in_zip, file_options)?; +pub fn extract_first_file_from_zip(zip_buf: &Vec) -> Result, std::io::Error> { + let reader = Cursor::new(zip_buf); + let mut archive = ZipArchive::new(reader)?; - let mut buff = Vec::new(); - file.read_to_end(&mut buff)?; - zip.write_all(&buff)?; + let mut result = None; - zip.finish()?; + for index in 0..archive.len() { + let mut entry = archive.by_index(index)?; + if entry.is_file() { + let mut buffer = String::new(); + entry.read_to_string(&mut buffer)?; + result = Some(buffer); + break; + } + } - let result = zip_path.to_str().ok_or(ZipError::InvalidArchive( - "Could not determine with zip filename", - ))?; - Ok(result.to_string()) + Ok(result) } diff --git a/openubl/ui/package.json b/openubl/ui/package.json index 4872eae2..8a946aa1 100644 --- a/openubl/ui/package.json +++ b/openubl/ui/package.json @@ -1,5 +1,5 @@ { - "name": "openubl-ui/root", + "name": "@openubl-ui/root", "version": "1.0.0", "license": "Apache-2.0", "private": true, diff --git a/xsender/src/file_sender.rs b/xsender/src/file_sender.rs index 1622e788..5958d43b 100644 --- a/xsender/src/file_sender.rs +++ b/xsender/src/file_sender.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use std::str::FromStr; use base64::engine::general_purpose; @@ -14,7 +15,7 @@ use crate::models::{Credentials, SendFileTarget, Urls, VerifyTicketTarget}; use crate::prelude::VerifyTicketStatus; use crate::soap::cdr::CdrMetadata; use crate::ubl_file::UblFile; -use crate::zip_manager::{create_zip, extract_cdr_from_base64_zip}; +use crate::zip_manager::{create_zip_from_str, decode_base64_zip_and_extract_first_file}; pub struct FileSender { pub urls: Urls, @@ -83,11 +84,14 @@ impl FileSender { &self.urls, ); - let zip = create_zip(&filename_without_extension, &xml.file_content.clone())?; + let zip_file_name = format!("{filename_without_extension}.zip"); + let file_name_inside_zip = format!("{filename_without_extension}.xml"); + + let zip = create_zip_from_str(&(xml.file_content), &file_name_inside_zip)?; let zip_base64 = general_purpose::STANDARD.encode(zip); let file_to_be_sent = File { - name: format!("{filename_without_extension}.zip"), + name: zip_file_name, base_64: zip_base64, }; @@ -98,7 +102,9 @@ impl FileSender { let response = match result { SendFileResponse::Cdr(cdr_base64) => { - let cdr_xml = extract_cdr_from_base64_zip(&cdr_base64)?; + let cdr_xml = decode_base64_zip_and_extract_first_file(&cdr_base64)?.ok_or( + FileSenderErr::Any(anyhow!("Could not extract the first file from zip")), + )?; let cdr_metadata = CdrMetadata::from_str(&cdr_xml)?; SendFileAggregatedResponse::Cdr(cdr_base64, cdr_metadata) } @@ -128,7 +134,9 @@ impl FileSender { let response = match result { VerifyTicketResponse::Cdr(status) => { - let cdr_xml = extract_cdr_from_base64_zip(&status.cdr_base64)?; + let cdr_xml = decode_base64_zip_and_extract_first_file(&status.cdr_base64)?.ok_or( + FileSenderErr::Any(anyhow!("Could not extract the first file from zip")), + )?; let cdr_metadata = CdrMetadata::from_str(&cdr_xml)?; VerifyTicketAggregatedResponse::Cdr(status, cdr_metadata) } diff --git a/xsender/src/zip_manager.rs b/xsender/src/zip_manager.rs index d9d870ec..848643d1 100644 --- a/xsender/src/zip_manager.rs +++ b/xsender/src/zip_manager.rs @@ -6,7 +6,7 @@ use zip::result::ZipResult; use zip::write::FileOptions; use zip::{ZipArchive, ZipWriter}; -pub fn create_zip(filename_without_extension: &str, content: &str) -> ZipResult> { +pub fn create_zip_from_str(content: &str, file_name_inside_zip: &str) -> ZipResult> { let mut data = Vec::new(); { @@ -14,7 +14,7 @@ pub fn create_zip(filename_without_extension: &str, content: &str) -> ZipResult< let mut zip = ZipWriter::new(buff); let file_options = FileOptions::default(); - zip.start_file(format!("{filename_without_extension}.xml"), file_options)?; + zip.start_file(file_name_inside_zip, file_options)?; zip.write_all(content.as_bytes())?; zip.finish()?; } @@ -22,32 +22,40 @@ pub fn create_zip(filename_without_extension: &str, content: &str) -> ZipResult< Ok(data) } -pub fn extract_cdr_from_base64_zip(base64: &str) -> anyhow::Result { +pub fn decode_base64_zip_and_extract_first_file(base64: &str) -> anyhow::Result> { let zip_buf = general_purpose::STANDARD.decode(base64)?; + Ok(extract_first_file_from_zip(&zip_buf)?) +} + +pub fn extract_first_file_from_zip(zip_buf: &Vec) -> Result, std::io::Error> { let reader = Cursor::new(zip_buf); let mut archive = ZipArchive::new(reader)?; - let mut cdr_xml_content = "".to_string(); + let mut result = None; + for index in 0..archive.len() { let mut entry = archive.by_index(index)?; - if entry.is_file() && entry.name().ends_with(".xml") { - entry.read_to_string(&mut cdr_xml_content)?; + if entry.is_file() { + let mut buffer = String::new(); + entry.read_to_string(&mut buffer)?; + result = Some(buffer); break; } } - Ok(cdr_xml_content) + Ok(result) } #[cfg(test)] mod tests { - use crate::zip_manager::extract_cdr_from_base64_zip; + use crate::zip_manager::decode_base64_zip_and_extract_first_file; #[test] fn read_base64_zip() { let cdr = "UEsDBBQAAgAIAEwklVcAAAAAAgAAAAAAAAAGAAAAZHVtbXkvAwBQSwMEFAACAAgATCSVVwwGkuIxBAAAGQ0AABsAAABSLTEyMzQ1Njc4OTEyLTAxLUYwMDEtMS54bWy1V2FP2zwQ/r5fEZUPk6Y3OElbWKPQqVDGsgFitDC0byY52uhN7WA7peXXv+e4SdMStHbSq/LBuXvuubvHZ1sEXxaz1JqDkAlnJy330GlZwCIeJ2xy0robf7U/t770PwRU+IMsS5OIKgTegsw4k2BhMJMnrVwwn1OZSJ/RGUhfZhAlTyuwnz+mvoymMKP+QsZ+yOY8icD2Wibcp2JPhoZK1mywUHvSnfHZjLPzhQKmVcBPpASm5Jo0eoz+ivQU4VEjIf07wsFkImBCFTSRxrgVU6Uyn5CXl5fDl/YhFxPiOY5DnB5BTCyTyUGJlpxmFd4kkofo0vYiUC8IsDmkPANSJcHkVRgsZKoKsDZLm7LYVgn2UiUp+5Q5o+rdPjMQeb3ZkUY39eqWxIv3enXJw9XlqKAqscgCi6yhaHTkKRU2egVIvfmy1Q9wgvy708tqIGQ55g0+Y6nNDsOV6gejZIId5KI6IjvsCx4zHQZxyJ54/4NlBWeUcYY6pclrodUVqCmPrUE64SJR09m7EriOpsW+IjtyO+zgF6L1AGkNW6TgrircmdTplLXaMy7gQEhqyyntut6K8haeQODtAdbdbajlQiOax4Iy+cTFTBpD3fTHtBsSlcMY27Ks3qTek3QXgZCQbFceDJMJSLWnYqjIQV2niueepjn0H16PRPsBEvXt+ntGw3m4hMvrQXZ+OoB0zKYv98+/w2V4/hS/dm/uur+PB/C8OLvoEX41ypxzGf5KLn9cPF9E38/vczn8OVVikRz9PDkJSD2L3h9SbRCOGtmctfpEmIhPNyKZ4+mz/oWl9fEUFL3Bo4rXGQj10WJcWXn2ydDUooIfsCw4g4eu0xtSRc1KR5kzj8zXeA3EVrQ2rfhNQmSo8W8HF2yhlDmIEYiEpnWLJt6fvhZbcBne63z2CGJ/to3oeoKyXLJWhlRqrXXEdfOdQt5ePm9Msh/gW6VN9+ZND4d979AJyBtrgTvLpeKz1e2CRreEbjsKtAYcO20XJ/mo2z72DLTy6iaHeos8x+3Zrmd7nbHj+MXfClpB1hFjfC76DbDCXsDKN37F7bULbtdgN5wbcEPc8dsd3+tuglfcNPJrqq960ZbR3fVgXOuuAnKxvKFCLY2tWIYxbk71mlU0qEEbf16v210TkfejSoeZQh1QrGqVGA/ZQpL3isPDnyiaVg0OlKLRdFZMkvbrkRGMpus7wUzObdg/2NJA20yihiDyp2RkW2f9CSwG8f9ISRoT3EIEyXznnK7X7nSPjj/3XG/nnA0phjzKtQrl4JW1VF/FUK60xBRf8dWwq8le2zcG+4zHONibE13YCtQQZCSSrKjrklpfaYSyU4thHYJbJsE/1pRaMom5RSPIFI2pYavHlj3VC1+3szEzW4VXUjXBjU5JlqB9x704wvNe/fbZjY0spHk/SPN/Nv3/AFBLAQIAABQAAgAIAEwklVcAAAAAAgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAABkdW1teS9QSwECAAAUAAIACABMJJVXDAaS4jEEAAAZDQAAGwAAAAAAAAABAAAAAAAmAAAAUi0xMjM0NTY3ODkxMi0wMS1GMDAxLTEueG1sUEsFBgAAAAACAAIAfQAAAJAEAAAAAA=="; - let result = extract_cdr_from_base64_zip(cdr).expect("Could not extract cdr file"); - assert!(!result.is_empty()); + let result = + decode_base64_zip_and_extract_first_file(cdr).expect("Could not extract cdr file"); + assert!(result.is_some()); } }