2023-12-21 16:31:21 +00:00
|
|
|
use anyhow::{Context, Result, anyhow};
|
|
|
|
use url::Url;
|
|
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
use fully_pub::fully_pub;
|
|
|
|
use crate::utils::Id;
|
|
|
|
use chrono::prelude::{NaiveDate, DateTime, Utc, Datelike};
|
2023-12-26 15:09:49 +00:00
|
|
|
use thiserror::Error;
|
2023-12-21 16:31:21 +00:00
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct HelloassoReferences {
|
|
|
|
answer_id: u64,
|
|
|
|
order_id: u64
|
|
|
|
// payment_id: u64,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct ExternalReferences {
|
|
|
|
helloasso_ref: HelloassoReferences
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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, Clone)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct User {
|
|
|
|
id: Id,
|
2023-12-26 15:09:49 +00:00
|
|
|
first_name: Option<String>,
|
2023-12-21 16:31:21 +00:00
|
|
|
last_name: String,
|
|
|
|
email: Option<String>,
|
|
|
|
|
|
|
|
phone: Option<String>,
|
|
|
|
address: String,
|
|
|
|
city: String,
|
|
|
|
postal_code: String,
|
|
|
|
country: String,
|
|
|
|
skills: Option<String>,
|
|
|
|
job: Option<String>,
|
2023-12-26 15:09:49 +00:00
|
|
|
birth_year: Option<u32>,
|
|
|
|
|
|
|
|
register_time: DateTime<Utc>
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct UserSummary {
|
|
|
|
id: Id,
|
|
|
|
first_name: Option<String>,
|
|
|
|
last_name: String,
|
|
|
|
email: Option<String>
|
2023-12-21 16:31:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[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<Id>,
|
|
|
|
campaign: String,
|
|
|
|
mode: MembershipMode,
|
2023-12-26 15:09:49 +00:00
|
|
|
inception_time: DateTime<Utc>,
|
2023-12-21 16:31:21 +00:00
|
|
|
external_references: ExternalReferences
|
|
|
|
}
|
|
|
|
|
2023-12-26 15:09:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct Client {
|
|
|
|
client: reqwest::Client,
|
|
|
|
base_url: Url,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Error, Debug)]
|
|
|
|
enum APIClientError {
|
|
|
|
#[error("Received non-normal status code from API")]
|
|
|
|
InvalidStatusCode
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct Credentials {
|
|
|
|
client_id: String,
|
|
|
|
client_secret: String
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Default for Client {
|
|
|
|
fn default() -> Self {
|
|
|
|
Client {
|
|
|
|
client: Client::get_base_client_builder()
|
|
|
|
.build()
|
|
|
|
.expect("Expected reqwest client to be built"),
|
|
|
|
base_url: Url::parse("https://paheko.etoiledebethleem.fr/api/") // the traling slash is important
|
|
|
|
.expect("Expected valid paheko API base URL")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
use base64_light::base64_encode;
|
|
|
|
|
|
|
|
fn build_auth_headers(credentials: &Credentials) -> reqwest::header::HeaderMap {
|
|
|
|
let mut login_headers = reqwest::header::HeaderMap::new();
|
|
|
|
login_headers.insert(
|
|
|
|
"Authorization",
|
|
|
|
format!("Basic {}", &base64_encode(
|
|
|
|
&format!("{}:{}", &credentials.client_id, &credentials.client_secret)
|
|
|
|
)).parse().expect("Header value to be OK")
|
|
|
|
);
|
|
|
|
login_headers
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Client {
|
|
|
|
pub fn new(base_url: String) -> Client {
|
|
|
|
Client {
|
|
|
|
client: Client::get_base_client_builder()
|
|
|
|
.build()
|
|
|
|
.expect("Expected reqwest client to be built"),
|
|
|
|
base_url: Url::parse(&base_url) // the traling slash is important
|
|
|
|
.expect("Expected valid paheko API base URL")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_base_client_builder() -> reqwest::ClientBuilder {
|
|
|
|
let mut default_headers = reqwest::header::HeaderMap::new();
|
|
|
|
default_headers.insert("Accept", "application/json".parse().unwrap());
|
|
|
|
|
|
|
|
let proxy = reqwest::Proxy::http("http://localhost:8998").unwrap();
|
|
|
|
reqwest::Client::builder()
|
|
|
|
.proxy(proxy)
|
|
|
|
.default_headers(default_headers)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn login(&mut self, credentials: Credentials) -> Result<AuthentifiedClient> {
|
|
|
|
let hypothetic_client = self.authentified_client(credentials);
|
|
|
|
let query: String = r#"
|
|
|
|
SELECT key,value FROM config WHERE key="org_name"
|
|
|
|
"#.to_string();
|
|
|
|
|
|
|
|
match hypothetic_client.sql_query(query).await {
|
|
|
|
Ok(_value) => {
|
|
|
|
Ok(hypothetic_client)
|
|
|
|
},
|
|
|
|
Err(err) => {
|
|
|
|
Err(anyhow!("Failed to authenticate: Credentials provided are invalids, {:?}", err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn authentified_client(&self, credentials: Credentials) -> AuthentifiedClient {
|
|
|
|
AuthentifiedClient::new(self.base_url.clone(), credentials)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct AuthentifiedClient {
|
|
|
|
credentials: Credentials,
|
|
|
|
client: reqwest::Client,
|
|
|
|
base_url: Url
|
|
|
|
}
|
|
|
|
|
|
|
|
// SELECT id,nom AS first_name,last_name,email,external_custom_data FROM users LIMIT 5;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct SimpleUser {
|
|
|
|
id: u32,
|
|
|
|
first_name: String,
|
|
|
|
last_name: String,
|
|
|
|
email: Option<String>,
|
|
|
|
external_custom_data: Option<String>
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct SqlQueryOutput {
|
|
|
|
count: u64,
|
|
|
|
results: serde_json::Value
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct TransactionSummary {
|
|
|
|
id: u64,
|
|
|
|
reference: String
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AuthentifiedClient {
|
|
|
|
pub fn new(base_url: Url, credentials: Credentials) -> Self {
|
|
|
|
AuthentifiedClient {
|
|
|
|
client: Client::get_base_client_builder()
|
|
|
|
.default_headers(build_auth_headers(&credentials))
|
|
|
|
.build()
|
|
|
|
.expect("Expect client to be built"),
|
|
|
|
credentials,
|
|
|
|
base_url
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn sql_query(&self, query: String) -> Result<SqlQueryOutput> {
|
|
|
|
#[derive(Serialize)]
|
|
|
|
struct Payload {
|
|
|
|
sql: String
|
|
|
|
}
|
|
|
|
let payload = Payload { sql: query };
|
|
|
|
let path = self.base_url.join("sql")?;
|
|
|
|
let res = self.client
|
|
|
|
.post(path)
|
|
|
|
.json(&payload)
|
|
|
|
.send().await?;
|
|
|
|
if res.status() != 200 {
|
|
|
|
dbg!(res);
|
|
|
|
return Err(APIClientError::InvalidStatusCode.into());
|
|
|
|
}
|
|
|
|
Ok(res.json().await.context("Sql query")?)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn get_users(&self) -> Result<Vec<UserSummary>> {
|
|
|
|
let query: String = r#"
|
|
|
|
SELECT id,nom AS first_name,last_name,email FROM users;
|
|
|
|
"#.to_string();
|
|
|
|
|
|
|
|
let users_val = self.sql_query(query).await.context("Fetching users")?;
|
|
|
|
|
|
|
|
Ok(serde_json::from_value(users_val.results)?)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn get_transactions(&self, id_year: u32)
|
|
|
|
-> Result<Vec<TransactionSummary>>
|
|
|
|
{
|
|
|
|
let query: String = format!(r#"
|
|
|
|
SELECT id,reference FROM acc_transactions WHERE id_year={} AND reference LIKE 'HA/%';
|
|
|
|
"#, id_year).to_string();
|
|
|
|
|
|
|
|
let val = self.sql_query(query).await.context("Fetching transactions")?;
|
|
|
|
|
|
|
|
Ok(serde_json::from_value(val.results)?)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn create_user(&self, user: &User)
|
|
|
|
-> Result<UserSummary>
|
|
|
|
{
|
|
|
|
// single-user import
|
|
|
|
// create virtual file
|
|
|
|
let u = user.clone();
|
|
|
|
|
|
|
|
let mut csv_content: String = String::new();
|
|
|
|
csv_content.push_str("numero,nom,last_name,adresse,code_postal,ville,pays,telephone,email,annee_naissance,profession,interets,lettre_infos,date_inscription\n");
|
|
|
|
csv_content.push_str(
|
|
|
|
format!("{},{:?},{:?},{:?},{:?},{:?},{:?},{:?},{:?},{},{:?},{:?},{},{}\n",
|
|
|
|
"".to_string(),
|
|
|
|
u.first_name.clone().unwrap_or("".to_string()),
|
|
|
|
u.last_name.clone(),
|
|
|
|
u.address,
|
|
|
|
u.postal_code,
|
|
|
|
u.city,
|
|
|
|
u.country,
|
|
|
|
u.phone.unwrap_or("".to_string()),
|
|
|
|
u.email.clone().unwrap_or("".to_string()),
|
|
|
|
u.birth_year.map(|x| format!("{}", x)).unwrap_or("".to_string()),
|
|
|
|
u.job.unwrap_or("".to_string()),
|
|
|
|
u.skills.unwrap_or("".to_string()),
|
|
|
|
1,
|
|
|
|
user.register_time.format("%d/%m/%Y")
|
|
|
|
).as_str());
|
|
|
|
|
|
|
|
use reqwest::multipart::Form;
|
|
|
|
use reqwest::multipart::Part;
|
|
|
|
|
|
|
|
let part = Part::text(csv_content).file_name("file");
|
|
|
|
|
|
|
|
let form = Form::new()
|
|
|
|
.part("file", part);
|
|
|
|
|
|
|
|
let res = self.client
|
|
|
|
.post(self.base_url.join("user/import/")?)
|
|
|
|
.multipart(form)
|
|
|
|
.send().await?;
|
|
|
|
|
|
|
|
if res.status() != 200 {
|
|
|
|
return Err(APIClientError::InvalidStatusCode.into());
|
|
|
|
}
|
|
|
|
Ok(
|
|
|
|
UserSummary {
|
|
|
|
id: Id(0),
|
|
|
|
first_name: u.first_name,
|
|
|
|
last_name: u.last_name,
|
|
|
|
email: u.email
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|