diff --git a/src/helloasso.rs b/src/helloasso.rs index c121d68..7558521 100644 --- a/src/helloasso.rs +++ b/src/helloasso.rs @@ -296,7 +296,7 @@ struct OrderDetails { #[serde(rename_all = "camelCase")] #[fully_pub] struct FormAnswer { - amount: u64, + amount: u32, #[serde(rename = "name")] mode: MembershipMode, diff --git a/src/main.rs b/src/main.rs index 1be8965..b5fb2bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,7 +4,7 @@ mod helloasso; use thiserror::Error; use anyhow::{Context, Result}; -use chrono::prelude::{NaiveDate, DateTime, Utc, Datelike}; +use chrono::prelude::{NaiveDate, NaiveDateTime, DateTime, Utc, Datelike}; use strum::Display; use serde::{Serialize, Deserialize}; use std::collections::HashSet; @@ -195,14 +195,14 @@ fn parse_and_get_birthday_year(raw_date: String) -> Option { Some(d.year().try_into().ok()?) } -fn helloasso_to_paheko_membership(helloasso_membership: &helloasso::MembershipMode) -> paheko::MembershipMode { - match helloasso_membership { - helloasso::MembershipMode::Couple => paheko::MembershipMode::Couple, - helloasso::MembershipMode::Individual => paheko::MembershipMode::Individual, - helloasso::MembershipMode::BenefactorCouple => paheko::MembershipMode::BenefactorCouple, - helloasso::MembershipMode::BenefactorIndividual => paheko::MembershipMode::BenefactorIndividual - } -} +// fn helloasso_to_paheko_membership(helloasso_membership: &helloasso::MembershipMode) -> paheko::MembershipMode { +// match helloasso_membership { +// helloasso::MembershipMode::Couple => paheko::MembershipMode::Couple, +// helloasso::MembershipMode::Individual => paheko::MembershipMode::Individual, +// helloasso::MembershipMode::BenefactorCouple => paheko::MembershipMode::BenefactorCouple, +// helloasso::MembershipMode::BenefactorIndividual => paheko::MembershipMode::BenefactorIndividual +// } +// } async fn launch_adapter() -> Result<()> { dotenvy::dotenv()?; @@ -243,8 +243,6 @@ async fn launch_adapter() -> Result<()> { // get the list of payments associated // first step: output a list of PahekoUser with PahekoMembership - let pk_memberships: Vec = vec![]; - let mut pk_users: Vec = vec![]; let mut pk_memberships: Vec = vec![]; // read_custom_field(&answer, HelloAssoCustomFieldType::Email).or(Some(answer.payer_user.email.clone())), @@ -268,9 +266,16 @@ async fn launch_adapter() -> Result<()> { // TODO: before creating any users, get the current maximum id of the users table to predict // the next auto-incrementing id. - let mut pk_next_id = paheko_client.get_user_next_id().await.context("Get paheko users next id")?; + let mut pk_next_user_id = paheko_client.get_next_id(&"users") + .await.context("Get paheko users next id")?; + let mut pk_next_user_service_id = paheko_client.get_next_id(&"services_users") + .await.context("Get paheko services_users next id")?; for answer in answers { + // list of users involved in this answer + let mut pk_users_summaries: Vec = vec![]; + let mut pk_user_service_registrations: Vec = vec![]; + eprintln!("Processing answer:"); let email = choose_email(&answer); @@ -318,29 +323,48 @@ async fn launch_adapter() -> Result<()> { Some(user) => user, None => { let c = paheko_client.create_user( - &pk_user, pk_next_id.clone() + &pk_user, pk_next_user_id.clone() ).await.context("Expected to create paheko user")?; eprintln!(" Created paheko user"); - pk_next_id += 1; + pk_next_user_id += 1; existing_users.push(c.clone()); c } }; + pk_users_summaries.push(pk_user_summary); let mut pk_membership = paheko::Membership { id: generate_id(), - campaign: "".to_string(), - inception_time: Utc::now(), - mode: helloasso_to_paheko_membership(&answer.mode), + campaign_name: "Cotisation 2023-2024".to_string(), + // FIXME: handle errors + mode_name: serde_json::to_value(answer.mode.clone()) + .unwrap().as_str().unwrap().to_string(), + start_time: answer.order.inception_time, + end_time: + DateTime::::from_naive_utc_and_offset( + NaiveDate::from_ymd_opt(2024, 12, 31).unwrap().and_hms_opt(23, 59, 59).unwrap(), + Utc + ), + payed_amount: f64::from(answer.amount)/100.0, users: vec![pk_user.id.clone()], external_references: paheko::ExternalReferences { - helloasso_ref: paheko::HelloassoReferences { + helloasso_refs: paheko::HelloassoReferences { answer_id: answer.id, order_id: answer.order.id } } }; + // add activity for first member + let user_registration = paheko_client.register_user_to_service( + pk_users_summaries.iter().nth(0).unwrap(), + &pk_membership, + pk_next_user_service_id.clone() + ).await.context("Registering user to paheko server")?; + pk_user_service_registrations.push(user_registration); + pk_next_user_service_id += 1; + + // then create optional linked user if answer.mode == helloasso::MembershipMode::Couple { let mut second_pk_user = pk_user.clone(); @@ -363,26 +387,46 @@ async fn launch_adapter() -> Result<()> { } if existing_user_opt.is_none() { - let second_pk_user_summary = paheko_client.create_user(&second_pk_user, pk_next_id) + let second_pk_user_summary = paheko_client.create_user(&second_pk_user, pk_next_user_id) .await.context("Expected to create second paheko user")?; eprintln!(" Created conjoint paheko user"); - pk_next_id += 1; + pk_users_summaries.push(second_pk_user_summary); + pk_next_user_id += 1; + + // create activity of second user + let user_registration = paheko_client.register_user_to_service( + pk_users_summaries.iter().nth(1).unwrap(), + &pk_membership, + pk_next_user_service_id.clone() + ).await.context("Registering service to second paheko server")?; + pk_user_service_registrations.push(user_registration); + pk_next_user_service_id += 1; + + // FIXME: follow the ids of the services registrations, to be able to later + // reference that user service } // TODO: get existing linked user from previous year pk_membership.users.push(second_pk_user.id.clone()); - pk_users.push(second_pk_user); } - // add activity - paheko_client.register_user_to_service(&pk_user_summary).await.context("Registering user to paheko server")?; + // add transaction + let transaction = paheko::SimpleTransaction { + label: "Adhésion Helloasso".to_string(), + amount: pk_membership.payed_amount, + reference: format!("HA/{}", pk_membership.external_references.helloasso_refs.answer_id), + credit_account_code: "756".to_string(), // cotisations account + debit_account_code: "512HA".to_string(), // helloasso account + inception_time: answer.order.inception_time, + kind: paheko::TransactionKind::Revenue, + linked_users: pk_users_summaries.iter().map(|x| x.id.clone()).collect(), + linked_services: pk_user_service_registrations.iter().map(|x| x.id.clone()).collect() + }; + let _ = paheko_client.register_transaction(transaction) + .await.context("Expected to create new paheko transaction"); - pk_users.push(pk_user); pk_memberships.push(pk_membership); } - dbg!(&pk_users); - dbg!(&pk_memberships); - dbg!(&pk_users.len()); dbg!(&pk_memberships.len()); Ok(()) diff --git a/src/paheko.rs b/src/paheko.rs index 624cc73..6245d9b 100644 --- a/src/paheko.rs +++ b/src/paheko.rs @@ -17,7 +17,7 @@ struct HelloassoReferences { #[derive(Debug, Serialize, Deserialize, Clone)] #[fully_pub] struct ExternalReferences { - helloasso_ref: HelloassoReferences + helloasso_refs: HelloassoReferences } /// for now we include the custom fields into the paheko user @@ -52,28 +52,48 @@ struct UserSummary { email: Option } -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -#[fully_pub] -enum MembershipMode { - Individual, - Couple, - BenefactorIndividual, - BenefactorCouple, -} - #[derive(Debug, Serialize, Deserialize, Clone)] #[fully_pub] struct Membership { id: Id, users: Vec, - campaign: String, - mode: MembershipMode, - inception_time: DateTime, - external_references: ExternalReferences + campaign_name: String, + mode_name: String, + start_time: DateTime, + end_time: DateTime, + payed_amount: f64, + external_references: ExternalReferences, } +#[derive(Debug, Clone)] +#[fully_pub] +enum TransactionKind { + Expense, + Revenue +} +impl Into for TransactionKind { + fn into(self) -> String { + match self { + TransactionKind::Expense => "EXPENSE".to_string(), + TransactionKind::Revenue => "REVENUE".to_string() + } + } +} +#[derive(Debug, Clone)] +#[fully_pub] +struct SimpleTransaction { + label: String, + kind: TransactionKind, + inception_time: DateTime, + amount: f64, + credit_account_code: String, + debit_account_code: String, + reference: String, + linked_users: Vec, + linked_services: Vec +} #[derive(Debug)] #[fully_pub] @@ -197,6 +217,12 @@ struct TransactionSummary { reference: String } +#[derive(Debug)] +#[fully_pub] +struct UserServiceRegistration { + id: Id +} + impl AuthentifiedClient { pub fn new(base_url: Url, credentials: Credentials) -> Self { AuthentifiedClient { @@ -237,21 +263,21 @@ impl AuthentifiedClient { Ok(serde_json::from_value(users_val.results)?) } - pub async fn get_user_next_id(&self) -> Result { - let query: String = r#" - SELECT id FROM users ORDER BY id DESC LIMIT 1 - "#.to_string(); + pub async fn get_next_id(&self, table_name: &str) -> Result { + let query: String = format!(r#" + SELECT id FROM {} ORDER BY id DESC LIMIT 1 + "#, table_name).to_string(); - let users_id_val = self.sql_query(query).await.context("Fetching users")?; + let data = self.sql_query(query).await.context("Fetching next id from table")?; #[derive(Deserialize)] - struct UserIdEntry { + struct Entry { id: u64 } - let users_ids: Vec = serde_json::from_value(users_id_val.results)?; + let ids: Vec = serde_json::from_value(data.results)?; - Ok(users_ids.iter().nth(0).map(|x| x.id).unwrap_or(1)+1) + Ok(ids.iter().nth(0).map(|x| x.id).unwrap_or(1)+1) } pub async fn get_transactions(&self, id_year: u32) @@ -319,8 +345,8 @@ impl AuthentifiedClient { ) } - pub async fn register_user_to_service(&self, user: &UserSummary) - -> Result<()> + pub async fn register_user_to_service(&self, user: &UserSummary, user_membership: &Membership, next_id: u64) + -> Result { // single-user import // create virtual file @@ -333,11 +359,11 @@ impl AuthentifiedClient { csv_content.push_str( format!("{},{:?},{:?},{:?},{:?},{:?},{:?}\n", u.id, - "Cotisation 2023-2024", - "Physique Individuelle", - "10/10/2023", - "10/10/2025", - "10", + user_membership.campaign_name, + user_membership.mode_name, + user_membership.start_time.format("%d/%m/%Y").to_string(), + user_membership.end_time.format("%d/%m/%Y").to_string(), + format!("{}", user_membership.payed_amount), "Oui" ).as_str()); @@ -354,6 +380,42 @@ impl AuthentifiedClient { .multipart(form) .send().await?; + if res.status() != 200 { + return Err(APIClientError::InvalidStatusCode.into()); + } + Ok(UserServiceRegistration { + id: Id(next_id) + }) + } + + pub async fn register_transaction(&self, transaction: SimpleTransaction) + -> Result<()> + { + use reqwest::multipart::Form; + + let mut form = Form::new() + .text("id_year", "1") + .text("label", transaction.label) + .text("date", transaction.inception_time.format("%d/%m/%Y").to_string()) + .text("type", Into::::into(transaction.kind)) + .text("amount", format!("{}", transaction.amount)) + .text("debit", transaction.debit_account_code) + .text("credit", transaction.credit_account_code) + .text("reference", transaction.reference) + ; + + for linked_id in transaction.linked_users { + form = form.text("linked_users[]", format!("{}", linked_id.0)); + } + for linked_id in transaction.linked_services { + form = form.text("linked_services[]", format!("{}", linked_id.0)); + } + + let res = self.client + .post(self.base_url.join("accounting/transaction")?) + .multipart(form) + .send().await?; + if res.status() != 200 { return Err(APIClientError::InvalidStatusCode.into()); }