feat: activity and transaction registration

This commit is contained in:
Matthieu Bessat 2023-12-28 02:23:34 +01:00
parent 1fcf35eaa7
commit 05eab87c3a
3 changed files with 163 additions and 57 deletions

View File

@ -296,7 +296,7 @@ struct OrderDetails {
#[serde(rename_all = "camelCase")]
#[fully_pub]
struct FormAnswer {
amount: u64,
amount: u32,
#[serde(rename = "name")]
mode: MembershipMode,

View File

@ -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<u32> {
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<paheko::Membership> = vec![];
let mut pk_users: Vec<paheko::User> = vec![];
let mut pk_memberships: Vec<paheko::Membership> = 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<paheko::UserSummary> = vec![];
let mut pk_user_service_registrations: Vec<paheko::UserServiceRegistration> = 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::<Utc>::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(())

View File

@ -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<String>
}
#[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,
inception_time: DateTime<Utc>,
external_references: ExternalReferences
campaign_name: String,
mode_name: String,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
payed_amount: f64,
external_references: ExternalReferences,
}
#[derive(Debug, Clone)]
#[fully_pub]
enum TransactionKind {
Expense,
Revenue
}
impl Into<String> 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<Utc>,
amount: f64,
credit_account_code: String,
debit_account_code: String,
reference: String,
linked_users: Vec<Id>,
linked_services: Vec<Id>
}
#[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<u64> {
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<u64> {
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<UserIdEntry> = serde_json::from_value(users_id_val.results)?;
let ids: Vec<Entry> = 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<UserServiceRegistration>
{
// 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::<String>::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());
}