use quick_xml::Reader;
use quick_xml::events::Event;
use crate::error::YukiError;
use super::local_name;
use super::soap_client::{SoapClient, SoapEnvelope};
const BASE_URL: &str = "https://api.yukiworks.nl/ws/Accounting.asmx";
#[derive(Debug, Clone)]
pub struct Administration {
pub id: String,
pub name: String,
pub domain_id: String,
}
#[derive(Debug, Clone)]
pub struct OutstandingItem {
pub contact_name: String,
pub description: String,
pub date: String,
pub amount: String,
pub open_amount: String,
}
#[derive(Debug, Clone)]
pub struct GlTransaction {
pub id: String,
pub date: String,
pub description: String,
pub gl_account: String,
pub amount: String,
}
#[derive(Debug, Clone)]
pub struct GlTransactionWithContact {
pub id: String,
pub date: String,
pub description: String,
pub gl_account: String,
pub amount: String,
pub contact_name: String,
}
pub struct AccountingClient {
soap: SoapClient,
}
impl AccountingClient {
pub fn new() -> Self {
Self {
soap: SoapClient::new(BASE_URL),
}
}
fn require_session(&self) -> Result<&str, YukiError> {
self.soap.session_id().ok_or_else(|| {
YukiError::AuthFailed("not authenticated — call authenticate() first".to_string())
})
}
pub async fn authenticate(&mut self, api_key: &str) -> Result<String, YukiError> {
self.soap.authenticate(api_key).await
}
pub async fn administrations(&self) -> Result<Vec<Administration>, YukiError> {
let session = self.require_session()?;
let envelope = SoapEnvelope::new("Administrations")
.session(session)
.build();
let body = self.soap.call("Administrations", envelope).await?;
Self::parse_administrations(&body)
}
pub async fn set_current_domain(&mut self, domain_id: &str) -> Result<(), YukiError> {
let session = self.require_session()?;
let envelope = SoapEnvelope::new("SetCurrentDomain")
.session(session)
.param("domainID", domain_id)
.build();
self.soap.call("SetCurrentDomain", envelope).await?;
Ok(())
}
pub async fn gl_account_balance(
&self,
administration_id: &str,
gl_account_code: &str,
transaction_date: &str,
) -> Result<String, YukiError> {
let session = self.require_session()?;
let envelope = SoapEnvelope::new("GLAccountBalance")
.session(session)
.param("administrationID", administration_id)
.param("GLAccountCode", gl_account_code)
.param("transactionDate", transaction_date)
.build();
self.soap.call("GLAccountBalance", envelope).await
}
pub async fn gl_account_transactions(
&self,
administration_id: &str,
gl_account_code: &str,
start_date: &str,
end_date: &str,
) -> Result<String, YukiError> {
let session = self.require_session()?;
let envelope = SoapEnvelope::new("GLAccountTransactions")
.session(session)
.param("administrationID", administration_id)
.param("GLAccountCode", gl_account_code)
.param("StartDate", start_date)
.param("EndDate", end_date)
.build();
self.soap.call("GLAccountTransactions", envelope).await
}
pub async fn outstanding_debtor_items(
&self,
administration_id: &str,
) -> Result<Vec<OutstandingItem>, YukiError> {
let session = self.require_session()?;
let envelope = SoapEnvelope::new("OutstandingDebtorItems")
.session(session)
.param("administrationID", administration_id)
.build();
let body = self.soap.call("OutstandingDebtorItems", envelope).await?;
Self::parse_outstanding_items(&body, "OutstandingDebtorItemsResult")
}
pub async fn outstanding_debtor_items_by_date(
&self,
administration_id: &str,
start_date: &str,
end_date: &str,
) -> Result<Vec<OutstandingItem>, YukiError> {
let session = self.require_session()?;
let envelope = SoapEnvelope::new("OutstandingDebtorItemsByDate")
.session(session)
.param("administrationID", administration_id)
.param("startDate", start_date)
.param("endDate", end_date)
.build();
let body = self
.soap
.call("OutstandingDebtorItemsByDate", envelope)
.await?;
Self::parse_outstanding_items(&body, "OutstandingDebtorItemsByDateResult")
}
pub async fn outstanding_creditor_items(
&self,
administration_id: &str,
) -> Result<Vec<OutstandingItem>, YukiError> {
let session = self.require_session()?;
let envelope = SoapEnvelope::new("OutstandingCreditorItems")
.session(session)
.param("administrationID", administration_id)
.build();
let body = self.soap.call("OutstandingCreditorItems", envelope).await?;
Self::parse_outstanding_items(&body, "OutstandingCreditorItemsResult")
}
pub async fn outstanding_creditor_items_by_date(
&self,
administration_id: &str,
start_date: &str,
end_date: &str,
) -> Result<Vec<OutstandingItem>, YukiError> {
let session = self.require_session()?;
let envelope = SoapEnvelope::new("OutstandingCreditorItemsByDate")
.session(session)
.param("administrationID", administration_id)
.param("startDate", start_date)
.param("endDate", end_date)
.build();
let body = self
.soap
.call("OutstandingCreditorItemsByDate", envelope)
.await?;
Self::parse_outstanding_items(&body, "OutstandingCreditorItemsByDateResult")
}
pub async fn gl_account_transactions_and_contact(
&self,
administration_id: &str,
gl_account_code: &str,
start_date: &str,
end_date: &str,
) -> Result<Vec<GlTransactionWithContact>, YukiError> {
let session = self.require_session()?;
let envelope = SoapEnvelope::new("GLAccountTransactionsAndContact")
.session(session)
.param("administrationID", administration_id)
.param("GLAccountCode", gl_account_code)
.param("StartDate", start_date)
.param("EndDate", end_date)
.build();
let body = self
.soap
.call("GLAccountTransactionsAndContact", envelope)
.await?;
Self::parse_gl_transactions_with_contact(&body)
}
pub async fn net_revenue(
&self,
administration_id: &str,
start_date: &str,
end_date: &str,
) -> Result<String, YukiError> {
let session = self.require_session()?;
let envelope = SoapEnvelope::new("NetRevenue")
.session(session)
.param("administrationID", administration_id)
.param("StartDate", start_date)
.param("EndDate", end_date)
.build();
let body = self.soap.call("NetRevenue", envelope).await?;
SoapClient::parse_single_result(&body, "NetRevenueResult")
}
pub async fn check_outstanding_item_admin(
&self,
administration_id: &str,
reference: &str,
) -> Result<String, YukiError> {
let session = self.require_session()?;
let envelope = SoapEnvelope::new("CheckOutstandingItemAdmin")
.session(session)
.param("administrationID", administration_id)
.param("Reference", reference)
.build();
self.soap.call("CheckOutstandingItemAdmin", envelope).await
}
pub fn parse_gl_transactions(xml: &str) -> Result<Vec<GlTransaction>, YukiError> {
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(true);
let mut transactions = Vec::new();
let mut in_transaction = false;
let mut field: Option<String> = None;
let mut current = GlTransaction {
id: String::new(),
date: String::new(),
description: String::new(),
gl_account: String::new(),
amount: String::new(),
};
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let local = local_name(e.name().as_ref()).to_string();
match local.as_str() {
"GLAccountTransaction" => {
in_transaction = true;
current = GlTransaction {
id: String::new(),
date: String::new(),
description: String::new(),
gl_account: String::new(),
amount: String::new(),
};
for attr in e.attributes().flatten() {
if attr.key.as_ref() == b"ID" {
current.id = String::from_utf8_lossy(&attr.value).to_string();
}
}
}
"Date" | "Description" | "Amount" | "GLAccountCode" if in_transaction => {
field = Some(local);
}
_ => {}
}
}
Ok(Event::Text(ref e)) => {
if let Some(ref f) = field {
let text = e
.unescape()
.map_err(|e| YukiError::Xml(e.to_string()))?
.trim()
.to_string();
match f.as_str() {
"Date" => current.date = text,
"Description" => current.description = text,
"Amount" => current.amount = text,
"GLAccountCode" => current.gl_account = text,
_ => {}
}
}
}
Ok(Event::End(ref e)) => {
let local = local_name(e.name().as_ref()).to_string();
match local.as_str() {
"Date" | "Description" | "Amount" | "GLAccountCode" => {
field = None;
}
"GLAccountTransaction" if in_transaction => {
transactions.push(current.clone());
in_transaction = false;
}
_ => {}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(YukiError::Xml(e.to_string())),
_ => {}
}
buf.clear();
}
Ok(transactions)
}
pub fn parse_gl_transactions_with_contact(
xml: &str,
) -> Result<Vec<GlTransactionWithContact>, YukiError> {
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(true);
let mut transactions = Vec::new();
let mut in_transaction = false;
let mut field: Option<String> = None;
let mut current = GlTransactionWithContact {
id: String::new(),
date: String::new(),
description: String::new(),
gl_account: String::new(),
amount: String::new(),
contact_name: String::new(),
};
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let local = local_name(e.name().as_ref()).to_string();
match local.as_str() {
"GLAccountTransaction" => {
in_transaction = true;
current = GlTransactionWithContact {
id: String::new(),
date: String::new(),
description: String::new(),
gl_account: String::new(),
amount: String::new(),
contact_name: String::new(),
};
for attr in e.attributes().flatten() {
if attr.key.as_ref() == b"ID" {
current.id = String::from_utf8_lossy(&attr.value).to_string();
}
}
}
"Date" | "Description" | "Amount" | "GLAccountCode" | "Contact"
| "ContactName"
if in_transaction =>
{
field = Some(local);
}
_ => {}
}
}
Ok(Event::Text(ref e)) => {
if let Some(ref f) = field {
let text = e
.unescape()
.map_err(|e| YukiError::Xml(e.to_string()))?
.trim()
.to_string();
match f.as_str() {
"Date" => current.date = text,
"Description" => current.description = text,
"Amount" => current.amount = text,
"GLAccountCode" => current.gl_account = text,
"Contact" | "ContactName" => current.contact_name = text,
_ => {}
}
}
}
Ok(Event::End(ref e)) => {
let local = local_name(e.name().as_ref()).to_string();
match local.as_str() {
"Date" | "Description" | "Amount" | "GLAccountCode" | "Contact"
| "ContactName" => {
field = None;
}
"GLAccountTransaction" if in_transaction => {
transactions.push(current.clone());
in_transaction = false;
}
_ => {}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(YukiError::Xml(e.to_string())),
_ => {}
}
buf.clear();
}
Ok(transactions)
}
pub fn parse_administrations(xml: &str) -> Result<Vec<Administration>, YukiError> {
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(true);
let mut administrations = Vec::new();
let mut current_id = String::new();
let mut current_name = String::new();
let mut current_domain_id = String::new();
let mut in_administration = false;
let mut in_name = false;
let mut in_domain_id = false;
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let local = local_name(e.name().as_ref()).to_string();
match local.as_str() {
"Administration" => {
in_administration = true;
current_id.clear();
current_name.clear();
current_domain_id.clear();
for attr in e.attributes().flatten() {
if attr.key.as_ref() == b"ID" {
current_id = String::from_utf8_lossy(&attr.value).to_string();
}
}
}
"Name" if in_administration => in_name = true,
"DomainID" if in_administration => in_domain_id = true,
_ => {}
}
}
Ok(Event::Text(ref e)) => {
let text = e
.unescape()
.map_err(|e| YukiError::Xml(e.to_string()))?
.trim()
.to_string();
if in_name {
current_name = text;
} else if in_domain_id {
current_domain_id = text;
}
}
Ok(Event::End(ref e)) => {
let local = local_name(e.name().as_ref()).to_string();
match local.as_str() {
"Name" => in_name = false,
"DomainID" => in_domain_id = false,
"Administration" => {
if !current_id.is_empty() {
administrations.push(Administration {
id: current_id.clone(),
name: current_name.clone(),
domain_id: current_domain_id.clone(),
});
}
in_administration = false;
}
_ => {}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(YukiError::Xml(e.to_string())),
_ => {}
}
buf.clear();
}
Ok(administrations)
}
pub fn parse_outstanding_items(
xml: &str,
result_tag: &str,
) -> Result<Vec<OutstandingItem>, YukiError> {
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(true);
let mut items = Vec::new();
let mut in_result = false;
let mut in_item = false;
let mut field: Option<String> = None;
let mut current = OutstandingItem {
contact_name: String::new(),
description: String::new(),
date: String::new(),
amount: String::new(),
open_amount: String::new(),
};
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let local = local_name(e.name().as_ref()).to_string();
match local.as_str() {
tag if tag == result_tag => in_result = true,
"Item" if in_result => {
in_item = true;
current = OutstandingItem {
contact_name: String::new(),
description: String::new(),
date: String::new(),
amount: String::new(),
open_amount: String::new(),
};
}
"Contact" | "ContactName" | "Description" | "Date" | "Amount"
| "OriginalAmount" | "OpenAmount"
if in_item =>
{
field = Some(local);
}
_ => {}
}
}
Ok(Event::Text(ref e)) => {
if let Some(ref f) = field {
let text = e
.unescape()
.map_err(|e| YukiError::Xml(e.to_string()))?
.trim()
.to_string();
match f.as_str() {
"Contact" | "ContactName" => current.contact_name = text,
"Description" => current.description = text,
"Date" => current.date = text,
"Amount" | "OriginalAmount" => current.amount = text,
"OpenAmount" => current.open_amount = text,
_ => {}
}
}
}
Ok(Event::End(ref e)) => {
let local = local_name(e.name().as_ref()).to_string();
match local.as_str() {
"Contact" | "ContactName" | "Description" | "Date" | "Amount"
| "OriginalAmount" | "OpenAmount" => {
field = None;
}
"Item" if in_item => {
items.push(current.clone());
in_item = false;
}
tag if tag == result_tag => {
in_result = false;
}
_ => {}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(YukiError::Xml(e.to_string())),
_ => {}
}
buf.clear();
}
Ok(items)
}
}
impl Default for AccountingClient {
fn default() -> Self {
Self::new()
}
}