diff --git a/.gitignore b/.gitignore index 86691aa..b724e24 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target .env log.log +tmp diff --git a/Cargo.lock b/Cargo.lock index 733b057..4235200 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,15 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -297,6 +306,15 @@ version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -580,6 +598,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -1067,6 +1091,15 @@ dependencies = [ "waker-fn", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1125,6 +1158,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -1156,6 +1195,15 @@ dependencies = [ "value-bag", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "memchr" version = "2.6.4" @@ -1178,6 +1226,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1216,6 +1270,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -1250,6 +1314,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "oncemutex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -1309,10 +1379,12 @@ dependencies = [ "clap", "dotenvy", "envy", + "phonenumber", + "rand 0.8.5", "reqwest", "serde", "serde_json", - "strum", + "strum 0.25.0", "surf", "thiserror", "tokio", @@ -1355,6 +1427,27 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "phonenumber" +version = "0.3.3+8.13.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635f3e6288e4f01c049d89332a031bd74f25d64b6fb94703ca966e819488cd06" +dependencies = [ + "bincode", + "either", + "fnv", + "itertools", + "lazy_static", + "nom", + "quick-xml", + "regex", + "regex-cache", + "serde", + "serde_derive", + "strum 0.24.1", + "thiserror", +] + [[package]] name = "pin-project" version = "1.1.3" @@ -1452,6 +1545,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.33" @@ -1541,6 +1643,53 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-cache" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7b62d69743b8b94f353b6b7c3deb4c5582828328bcb8d5fedf214373808793" +dependencies = [ + "lru-cache", + "oncemutex", + "regex", + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "reqwest" version = "0.11.22" @@ -1896,13 +2045,35 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros 0.24.3", +] + [[package]] name = "strum" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" dependencies = [ - "strum_macros", + "strum_macros 0.25.2", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index feaf5c8..0b7f5f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,5 @@ chrono = { version = "0.4.31", features = ["serde"] } envy = "0.4.2" strum = { version = "0.25", features = ["derive"] } dotenvy = "0.15.7" +rand = "0.8.5" +phonenumber = "0.3.3" diff --git a/src/main.rs b/src/main.rs index cb6b30a..0e302ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,30 @@ -use url::Url; -use serde::{Serialize, Deserialize}; use anyhow::{Context, Result, anyhow}; +use url::Url; use chrono::prelude::{NaiveDate, DateTime, Utc}; -use strum::{Display }; +use strum::Display; +use serde::{Serialize, Deserialize}; use std::collections::HashSet; +use rand::{thread_rng, Rng}; +use phonenumber; + +/// ID +#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Hash)] +pub struct Id(pub u64); +impl Id { + pub fn to_string(&self) -> String { + format!("{:x}", self.0) + } +} +impl Into for Id { + fn into(self) -> String { + format!("{:x}", self.0) + } +} + +pub fn generate_id() -> Id { + Id(thread_rng().gen()) +} + /// permanent config to store long-term config /// used to ingest env settings @@ -321,7 +342,7 @@ struct Organization { slug: String } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] enum MembershipMode { #[serde(rename = "Individuel")] Individual, @@ -410,8 +431,9 @@ fn get_paheko_membership_from_ha_answers() { /// for now we include the custom fields into the paheko user /// we don't have time to implement user settings to change the custom fields mapping /// for now, manual mapping -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] struct PahekoUser { + id: Id, first_name: String, last_name: String, email: String, @@ -425,12 +447,12 @@ struct PahekoUser { birthday: Option // we will need to validate some data before } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] struct PahekoMembership { - author: PahekoUser, - linked_users: Vec, + id: Id, + users: Vec, campaign: String, - mode: String, + mode: MembershipMode, inception_datum: DateTime } @@ -484,6 +506,21 @@ fn read_custom_field(form_answer: &FormAnswer, custom_field: HelloAssoCustomFiel .and_then(|cf| Some(cf.answer.clone())) } +fn parse_normalize_phone(phone_number_opt: Option) -> Option { + let number_raw = phone_number_opt?; + + let parsed = match phonenumber::parse(Some(phonenumber::country::Id::FR), number_raw) { + Ok(r) => { + r + }, + Err(_e) => { + return None; + } + }; + + Some(parsed.to_string()) +} + async fn launch_adapter() -> Result<()> { dotenvy::dotenv()?; @@ -509,9 +546,13 @@ async fn launch_adapter() -> Result<()> { // dbg!(&answers); println!("Got {} answers to the membership form. Processing...", &answers.len()); + // first, request the current list of membership in paheko that were created with helloasso + // 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![]; let mut count: u64 = 0; let mut names: HashSet = HashSet::new(); @@ -523,25 +564,65 @@ async fn launch_adapter() -> Result<()> { names.insert(custom_field.name.clone()); count += 1; } - pk_users.push(PahekoUser { + let paheko_user = PahekoUser { + id: generate_id(), first_name: answer.user.first_name.clone(), last_name: answer.user.last_name.clone(), email: answer.user.email.clone(), - phone: read_custom_field(&answer, HelloAssoCustomFieldType::Phone), + phone: parse_normalize_phone(read_custom_field(&answer, HelloAssoCustomFieldType::Phone)), skills: read_custom_field(&answer, HelloAssoCustomFieldType::Skills), address: read_custom_field(&answer, HelloAssoCustomFieldType::Address).expect("to have address"), postal_code: read_custom_field(&answer, HelloAssoCustomFieldType::PostalCode).expect("to have postal code"), city: read_custom_field(&answer, HelloAssoCustomFieldType::City).expect("to have city"), job: read_custom_field(&answer, HelloAssoCustomFieldType::Job), birthday: None - }); + }; + let mut pk_membership = PahekoMembership { + id: generate_id(), + campaign: "".to_string(), + inception_datum: Utc::now(), + mode: answer.mode.clone(), + users: vec![paheko_user.id.clone()] + }; + dbg!(&pk_membership.users); // then create optional linked user + + if answer.mode == MembershipMode::Couple { + let mut second_pk_user = paheko_user.clone(); + second_pk_user.id = generate_id(); + + match read_custom_field(&answer, HelloAssoCustomFieldType::LinkedUserFirstName) { + Some(name) => { + second_pk_user.first_name = name + }, + None => { + second_pk_user.first_name = "Conjoint".to_string(); + eprintln!("Got a user with Couple mode but no additional name given!") + } + } + + pk_membership.users.push(second_pk_user.id.clone()); + pk_users.push(second_pk_user); + } + pk_users.push(paheko_user); + pk_memberships.push(pk_membership); } - dbg!(pk_users); - dbg!(names); - dbg!(count); + dbg!(&pk_users); + dbg!(&pk_memberships); + dbg!(&pk_users.len()); + dbg!(&pk_memberships.len()); + + + // println!("{:?}", &pk_users.iter().map(|user| format!("{:?}", user.email)).collect::>()); + + for u in pk_users { + println!("{} {}", u.email, u.phone.unwrap_or("".to_string())); + } + // then, request the current list of users + // match with the email address + // we consider the email address as the id for a helloasso user // then, upload the PahekoMembership Ok(())