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;
|
2023-12-28 10:40:38 +00:00
|
|
|
use chrono::prelude::{DateTime, Utc};
|
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 {
|
2023-12-28 01:23:34 +00:00
|
|
|
helloasso_refs: HelloassoReferences
|
2023-12-21 16:31:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct Membership {
|
|
|
|
id: Id,
|
|
|
|
users: Vec<Id>,
|
2023-12-28 01:23:34 +00:00
|
|
|
campaign_name: String,
|
|
|
|
mode_name: String,
|
|
|
|
start_time: DateTime<Utc>,
|
|
|
|
end_time: DateTime<Utc>,
|
|
|
|
payed_amount: f64,
|
|
|
|
external_references: ExternalReferences,
|
2023-12-21 16:31:21 +00:00
|
|
|
}
|
|
|
|
|
2023-12-28 01:23:34 +00:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
#[fully_pub]
|
|
|
|
enum TransactionKind {
|
|
|
|
Expense,
|
|
|
|
Revenue
|
|
|
|
}
|
2023-12-26 15:09:49 +00:00
|
|
|
|
2023-12-28 10:40:38 +00:00
|
|
|
impl From<TransactionKind> for String {
|
|
|
|
fn from(val: TransactionKind) -> Self {
|
|
|
|
match val {
|
2023-12-28 01:23:34 +00:00
|
|
|
TransactionKind::Expense => "EXPENSE".to_string(),
|
|
|
|
TransactionKind::Revenue => "REVENUE".to_string()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-12-26 15:09:49 +00:00
|
|
|
|
2023-12-28 01:23:34 +00:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct SimpleTransaction {
|
|
|
|
label: String,
|
|
|
|
kind: TransactionKind,
|
|
|
|
inception_time: DateTime<Utc>,
|
|
|
|
amount: f64,
|
|
|
|
credit_account_code: String,
|
|
|
|
debit_account_code: String,
|
|
|
|
reference: String,
|
|
|
|
linked_users: Vec<Id>,
|
2023-12-30 22:48:23 +00:00
|
|
|
linked_services: Vec<Id>,
|
|
|
|
accounting_year: Id
|
2023-12-28 01:23:34 +00:00
|
|
|
}
|
2023-12-26 15:09:49 +00:00
|
|
|
|
|
|
|
#[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
|
|
|
|
}
|
|
|
|
|
2023-12-30 22:48:23 +00:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct ClientConfig {
|
|
|
|
base_url: Url,
|
|
|
|
proxy: Option<reqwest::Proxy>,
|
|
|
|
user_agent: String
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for ClientConfig {
|
|
|
|
fn default() -> Self {
|
|
|
|
ClientConfig {
|
|
|
|
proxy: None,
|
|
|
|
base_url: Url::parse("https://paheko.example.org/api/") // the traling slash is important
|
|
|
|
.expect("Expected valid paheko API base URL"),
|
|
|
|
user_agent: "".to_string()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct Client {
|
|
|
|
client: reqwest::Client,
|
|
|
|
config: ClientConfig
|
|
|
|
}
|
2023-12-26 15:09:49 +00:00
|
|
|
|
|
|
|
impl Default for Client {
|
|
|
|
fn default() -> Self {
|
2023-12-30 22:48:23 +00:00
|
|
|
let base_config: ClientConfig = Default::default();
|
2023-12-26 15:09:49 +00:00
|
|
|
Client {
|
2023-12-30 22:48:23 +00:00
|
|
|
client: Client::get_base_client_builder(&base_config)
|
2023-12-26 15:09:49 +00:00
|
|
|
.build()
|
|
|
|
.expect("Expected reqwest client to be built"),
|
2023-12-30 22:48:23 +00:00
|
|
|
config: base_config
|
2023-12-26 15:09:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2023-12-30 22:48:23 +00:00
|
|
|
pub fn new(config: ClientConfig) -> Client {
|
2023-12-26 15:09:49 +00:00
|
|
|
Client {
|
2023-12-30 22:48:23 +00:00
|
|
|
client: Client::get_base_client_builder(&config)
|
2023-12-26 15:09:49 +00:00
|
|
|
.build()
|
|
|
|
.expect("Expected reqwest client to be built"),
|
2023-12-30 22:48:23 +00:00
|
|
|
config
|
2023-12-26 15:09:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-30 22:48:23 +00:00
|
|
|
fn get_base_client_builder(config: &ClientConfig) -> reqwest::ClientBuilder {
|
2023-12-26 15:09:49 +00:00
|
|
|
let mut default_headers = reqwest::header::HeaderMap::new();
|
|
|
|
default_headers.insert("Accept", "application/json".parse().unwrap());
|
2023-12-30 22:48:23 +00:00
|
|
|
default_headers.insert("User-Agent", config.user_agent.parse().unwrap());
|
2023-12-26 15:09:49 +00:00
|
|
|
|
2023-12-30 22:48:23 +00:00
|
|
|
let mut builder = reqwest::Client::builder()
|
|
|
|
.default_headers(default_headers);
|
|
|
|
if let Some(proxy) = &config.proxy {
|
|
|
|
builder = builder.proxy(proxy.clone());
|
|
|
|
}
|
|
|
|
builder
|
2023-12-26 15:09:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2023-12-30 22:48:23 +00:00
|
|
|
AuthentifiedClient::new(self.config.clone(), credentials)
|
2023-12-26 15:09:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
pub struct AuthentifiedClient {
|
2023-12-30 22:48:23 +00:00
|
|
|
_credentials: Credentials,
|
2023-12-26 15:09:49 +00:00
|
|
|
client: reqwest::Client,
|
2023-12-30 22:48:23 +00:00
|
|
|
config: ClientConfig
|
2023-12-26 15:09:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2023-12-28 01:23:34 +00:00
|
|
|
#[derive(Debug)]
|
|
|
|
#[fully_pub]
|
|
|
|
struct UserServiceRegistration {
|
|
|
|
id: Id
|
|
|
|
}
|
|
|
|
|
2023-12-26 15:09:49 +00:00
|
|
|
impl AuthentifiedClient {
|
2023-12-30 22:48:23 +00:00
|
|
|
pub fn new(config: ClientConfig, credentials: Credentials) -> Self {
|
2023-12-26 15:09:49 +00:00
|
|
|
AuthentifiedClient {
|
2023-12-30 22:48:23 +00:00
|
|
|
client: Client::get_base_client_builder(&config)
|
2023-12-26 15:09:49 +00:00
|
|
|
.default_headers(build_auth_headers(&credentials))
|
|
|
|
.build()
|
|
|
|
.expect("Expect client to be built"),
|
2023-12-30 22:48:23 +00:00
|
|
|
_credentials: credentials,
|
|
|
|
config
|
2023-12-26 15:09:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn sql_query(&self, query: String) -> Result<SqlQueryOutput> {
|
|
|
|
#[derive(Serialize)]
|
|
|
|
struct Payload {
|
|
|
|
sql: String
|
|
|
|
}
|
|
|
|
let payload = Payload { sql: query };
|
2023-12-30 22:48:23 +00:00
|
|
|
let path = self.config.base_url.join("sql")?;
|
2023-12-26 15:09:49 +00:00
|
|
|
let res = self.client
|
|
|
|
.post(path)
|
|
|
|
.json(&payload)
|
|
|
|
.send().await?;
|
|
|
|
if res.status() != 200 {
|
|
|
|
dbg!(res);
|
|
|
|
return Err(APIClientError::InvalidStatusCode.into());
|
|
|
|
}
|
2023-12-28 10:40:38 +00:00
|
|
|
res.json().await.context("Sql query")
|
2023-12-26 15:09:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)?)
|
|
|
|
}
|
|
|
|
|
2023-12-28 01:23:34 +00:00
|
|
|
pub async fn get_next_id(&self, table_name: &str) -> Result<u64> {
|
|
|
|
let query: String = format!(r#"
|
|
|
|
SELECT id FROM {} ORDER BY id DESC LIMIT 1
|
|
|
|
"#, table_name).to_string();
|
2023-12-27 00:03:33 +00:00
|
|
|
|
2023-12-28 01:23:34 +00:00
|
|
|
let data = self.sql_query(query).await.context("Fetching next id from table")?;
|
2023-12-27 00:03:33 +00:00
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
2023-12-28 01:23:34 +00:00
|
|
|
struct Entry {
|
2023-12-27 00:03:33 +00:00
|
|
|
id: u64
|
|
|
|
}
|
|
|
|
|
2023-12-28 01:23:34 +00:00
|
|
|
let ids: Vec<Entry> = serde_json::from_value(data.results)?;
|
2023-12-27 00:03:33 +00:00
|
|
|
|
2023-12-28 10:40:38 +00:00
|
|
|
Ok(ids.get(0).map(|x| x.id).unwrap_or(1)+1)
|
2023-12-27 00:03:33 +00:00
|
|
|
}
|
|
|
|
|
2023-12-26 15:09:49 +00:00
|
|
|
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)?)
|
|
|
|
}
|
|
|
|
|
2023-12-27 00:03:33 +00:00
|
|
|
pub async fn create_user(&self, user: &User, next_id: u64)
|
2023-12-26 15:09:49 +00:00
|
|
|
-> 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
|
2023-12-30 22:48:23 +00:00
|
|
|
.post(self.config.base_url.join("user/import/")?)
|
2023-12-26 15:09:49 +00:00
|
|
|
.multipart(form)
|
|
|
|
.send().await?;
|
|
|
|
|
|
|
|
if res.status() != 200 {
|
|
|
|
return Err(APIClientError::InvalidStatusCode.into());
|
|
|
|
}
|
|
|
|
Ok(
|
|
|
|
UserSummary {
|
2023-12-27 00:03:33 +00:00
|
|
|
id: Id(next_id),
|
2023-12-26 15:09:49 +00:00
|
|
|
first_name: u.first_name,
|
|
|
|
last_name: u.last_name,
|
|
|
|
email: u.email
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
2023-12-27 00:03:33 +00:00
|
|
|
|
2023-12-28 01:23:34 +00:00
|
|
|
pub async fn register_user_to_service(&self, user: &UserSummary, user_membership: &Membership, next_id: u64)
|
|
|
|
-> Result<UserServiceRegistration>
|
2023-12-27 00:03:33 +00:00
|
|
|
{
|
|
|
|
// single-user import
|
|
|
|
// create virtual file
|
|
|
|
let u = user.clone();
|
|
|
|
|
|
|
|
let mut csv_content: String = String::new();
|
|
|
|
csv_content.push_str(
|
|
|
|
r#""Numéro de membre","Activité","Tarif","Date d'inscription","Date d'expiration","Montant à régler","Payé ?""#);
|
2023-12-28 10:40:38 +00:00
|
|
|
csv_content.push('\n');
|
2023-12-27 00:03:33 +00:00
|
|
|
csv_content.push_str(
|
|
|
|
format!("{},{:?},{:?},{:?},{:?},{:?},{:?}\n",
|
|
|
|
u.id,
|
2023-12-28 01:23:34 +00:00
|
|
|
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),
|
2023-12-27 00:03:33 +00:00
|
|
|
"Oui"
|
|
|
|
).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
|
2023-12-30 22:48:23 +00:00
|
|
|
.post(self.config.base_url.join("services/subscriptions/import")?)
|
2023-12-27 00:03:33 +00:00
|
|
|
.multipart(form)
|
|
|
|
.send().await?;
|
|
|
|
|
2023-12-28 01:23:34 +00:00
|
|
|
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()
|
2023-12-30 22:48:23 +00:00
|
|
|
.text("id_year", transaction.accounting_year.to_string())
|
2023-12-28 01:23:34 +00:00
|
|
|
.text("label", transaction.label)
|
|
|
|
.text("date", transaction.inception_time.format("%d/%m/%Y").to_string())
|
|
|
|
.text("type", Into::<String>::into(transaction.kind))
|
|
|
|
.text("amount", format!("{}", transaction.amount))
|
|
|
|
.text("debit", transaction.debit_account_code)
|
|
|
|
.text("credit", transaction.credit_account_code)
|
2023-12-30 22:48:23 +00:00
|
|
|
// "Numéro pièce comptable" enregistré au niveau de la transaction
|
|
|
|
.text("reference", transaction.reference)
|
2023-12-28 01:23:34 +00:00
|
|
|
;
|
|
|
|
|
|
|
|
for linked_id in transaction.linked_users {
|
|
|
|
form = form.text("linked_users[]", format!("{}", linked_id.0));
|
|
|
|
}
|
2023-12-30 22:48:23 +00:00
|
|
|
// only possible with paheko fork
|
2023-12-28 01:23:34 +00:00
|
|
|
for linked_id in transaction.linked_services {
|
|
|
|
form = form.text("linked_services[]", format!("{}", linked_id.0));
|
|
|
|
}
|
|
|
|
|
|
|
|
let res = self.client
|
2023-12-30 22:48:23 +00:00
|
|
|
.post(self.config.base_url.join("accounting/transaction")?)
|
2023-12-28 01:23:34 +00:00
|
|
|
.multipart(form)
|
|
|
|
.send().await?;
|
|
|
|
|
2023-12-27 00:03:33 +00:00
|
|
|
if res.status() != 200 {
|
|
|
|
return Err(APIClientError::InvalidStatusCode.into());
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
2023-12-26 15:09:49 +00:00
|
|
|
}
|