commit
ba21d0dada
@ -0,0 +1,2 @@ |
||||
/target |
||||
*.env |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,15 @@ |
||||
[package] |
||||
name = "FSS" |
||||
version = "0.1.0" |
||||
edition = "2021" |
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||
|
||||
[dependencies] |
||||
tiberius = {version = "0.11.2", features = ["winauth", "rust_decimal", "chrono"]} |
||||
tokio = {version = "1.21.2", features = ["full"]} |
||||
tokio-util = {version = "0.7.4", features = ["full"]} |
||||
connectorx = {version = "0.2.5"} |
||||
polars = {version = "0.24.3"} |
||||
dotenv = "0.15.0" |
||||
|
||||
@ -0,0 +1,20 @@ |
||||
[package] |
||||
name = "FSS" |
||||
version = "0.1.0" |
||||
edition = "2021" |
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||
|
||||
[dependencies] |
||||
tiberius = {version = "0.11.2", features = ["winauth", "rust_decimal", "chrono"]} |
||||
tokio = {version = "1.21.2", features = ["full"]} |
||||
tokio-util = {version = "0.7.4", features = ["full"]} |
||||
chrono = {version = "0.4.22", features = ["serde"]} |
||||
actix-web = "4" |
||||
serde = {version = "1.0.147", features = ["derive"]} |
||||
serde_json = "1.0.87" |
||||
walkdir = "2.3.2" |
||||
|
||||
[target.x86_64-pc-windows-gnu] |
||||
linker = "/usr/bin/$ARCH-w64-mingw32-gcc" |
||||
ar = "/usr/$ARCH-w64-mingw32/bin/ar" |
||||
@ -0,0 +1,120 @@ |
||||
use chrono::NaiveDateTime; |
||||
use tiberius::{Client, Config, Query, AuthMethod, Row}; |
||||
use tokio::net::TcpStream; |
||||
use tokio_util::compat::{TokioAsyncWriteCompatExt, Compat}; |
||||
mod pymt_trans_table; |
||||
use pymt_trans_table::{PaymentData, FromSqlRow}; |
||||
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, dev::Payload}; |
||||
use serde::{Serialize, Deserialize}; |
||||
use dotenv::dotenv; |
||||
use std::env::var; |
||||
|
||||
|
||||
type BDynError = Box< dyn std::error::Error>; |
||||
|
||||
#[actix_web::main] |
||||
async fn main() -> Result<(), BDynError> { |
||||
HttpServer::new(|| { |
||||
App::new() |
||||
.service(search_contract) |
||||
}) |
||||
.bind(("127.0.0.1", 8080))? |
||||
.run() |
||||
.await; |
||||
Ok(()) |
||||
} |
||||
|
||||
#[derive(Deserialize)] |
||||
struct ContractNumber { |
||||
contract_number: String |
||||
} |
||||
|
||||
#[derive(Serialize)] |
||||
struct SearchResponse { |
||||
pub paid_in_full : Vec<PaymentData>, |
||||
pub checks: Option<Vec<[String;2]>> |
||||
} |
||||
|
||||
#[get("/contract/{contract_number}")] |
||||
async fn search_contract(contract_number: web::Path<String>) -> HttpResponse { |
||||
let mut resp = SearchResponse{paid_in_full: vec![], checks: None}; |
||||
dotenv().ok(); |
||||
let mut config = Config::new(); |
||||
config.host("172.16.181.143"); |
||||
config.port(1433); |
||||
config.authentication(AuthMethod::windows(var("USER_NAME").except("No USER_NAME set in .env"), var("PASSWORD").except("No PASSWORD set in .env"))); |
||||
config.trust_cert(); // on production, it is not a good idea to do this
|
||||
|
||||
let paid_in_full = get_paid_in_full(config.clone(), contract_number.to_string()).await; |
||||
let mut checks = vec![]; |
||||
match paid_in_full { |
||||
Ok(pif) => { |
||||
if pif.len() > 0 { |
||||
for payment in pif { |
||||
if payment.is_check() { |
||||
checks.push(payment.check_info()); |
||||
} |
||||
resp.paid_in_full.push(payment); |
||||
} |
||||
let found_checks = find_checks(config, checks).await; |
||||
match found_checks { |
||||
Ok(check_list) => {resp.checks = Some(check_list);}, |
||||
Err(e) => {return HttpResponse::NotFound().body("Contract not found")} |
||||
} |
||||
}; |
||||
}, |
||||
Err(e) => {return HttpResponse::NotFound().body("Contract not found")} |
||||
} |
||||
HttpResponse::Ok().body(serde_json::json!(resp).to_string()) |
||||
} |
||||
|
||||
async fn create_sql_conn(config: Config) -> Result<Client<Compat<TcpStream>>, BDynError> { |
||||
let tcp = TcpStream::connect(config.get_addr()).await?; |
||||
tcp.set_nodelay(true)?; |
||||
|
||||
Ok(Client::connect(config, tcp.compat_write()).await?) |
||||
} |
||||
|
||||
fn create_table_struct<T>(sql_result: Vec<Row>) -> Result<Vec<T>, BDynError> |
||||
where T: FromSqlRow<T> |
||||
{ |
||||
let mut table: Vec<T> = vec![]; |
||||
for row in sql_result { |
||||
let raw_data = T::from_sql_row(row); |
||||
match raw_data { |
||||
Ok(structure_data) => table.push(structure_data), |
||||
Err(e) => {} |
||||
} |
||||
} |
||||
Ok(table) |
||||
} |
||||
|
||||
async fn get_paid_in_full(config: Config, contract_number: String) -> Result<Vec<PaymentData>, BDynError> { |
||||
let mut client = create_sql_conn(config).await?; |
||||
let pif_query = Query::new(format!("USE NLCF SELECT TRXDSCRN as ContractNumber, BACHNUMB, VENDORID, PRCHAMNT, DOCDATE, DEX_ROW_TS FROM PM30200 WHERE TRXDSCRN = '{}'", contract_number)); |
||||
let stream = pif_query.query(&mut client).await?; |
||||
let row = stream.into_first_result().await?; |
||||
create_table_struct(row) |
||||
} |
||||
|
||||
async fn find_checks(config: Config, search_data: Vec<(String, String, NaiveDateTime)>) -> Result<Vec<[String;2]>, BDynError>{ |
||||
use walkdir::{WalkDir, DirEntry}; |
||||
let mut client = create_sql_conn(config).await?; |
||||
let mut found_checks: Vec<[String;2]> = vec![]; |
||||
|
||||
for (vendor_id, bach_num, timestamp) in search_data { |
||||
let check_query = Query::new(format!("USE NLCF SELECT VENDORID, CHEKNUMB, CHEKAMNT FROM ME240461 |
||||
WHERE VENDORID = '{}' AND BACHNUMB= '{}' AND DOCDATE >= {}", vendor_id, bach_num, timestamp.format("%Y-%m-%d"))); |
||||
let stream = check_query.query(&mut client).await?; |
||||
let rows = stream.into_first_result().await?; |
||||
if rows.len() < 1 {continue} |
||||
for row in rows { |
||||
let check_number: &str = row.get("CHEKNUMB").unwrap(); |
||||
let vendor: &str = row.get("VENDORID").unwrap(); |
||||
let files: Vec<DirEntry> = WalkDir::new(&format!("\\\\LCCPHL-SSFSH001\\Mekorma PDF\\NLCF\\PROCESSED\\*{}*",check_number)).into_iter().filter_map(|e| e.ok()).collect(); |
||||
if files.len() > 0 {found_checks.push([vendor.to_string(), files[0].file_name().to_str().unwrap().to_string()])} |
||||
} |
||||
} |
||||
Ok(found_checks) |
||||
} |
||||
|
||||
@ -0,0 +1,50 @@ |
||||
|
||||
use tiberius::{Row, numeric::Decimal}; |
||||
use chrono::NaiveDateTime; |
||||
use serde::{Serialize}; |
||||
|
||||
type BDynError = Box< dyn std::error::Error>; |
||||
|
||||
#[derive(Debug, Serialize)] |
||||
pub struct PaymentData { |
||||
contract_number: String, |
||||
bach_num: String, |
||||
vendor_id: String, |
||||
purchase_amount: Decimal, |
||||
doc_date: NaiveDateTime, |
||||
dex_row_ts: NaiveDateTime, |
||||
} |
||||
impl PaymentData { |
||||
pub fn is_check(&self) -> bool { |
||||
self.bach_num.find("CHK").is_some() |
||||
} |
||||
pub fn check_info(&self) -> (String, String, NaiveDateTime) { |
||||
return (self.vendor_id.clone(), self.bach_num.clone(), self.dex_row_ts.clone()) |
||||
} |
||||
} |
||||
|
||||
impl FromSqlRow<PaymentData> for PaymentData { |
||||
|
||||
fn from_sql_row(sql_data: Row) -> Result<Self, BDynError> { |
||||
let cn: &str = sql_data.get("ContractNumber").unwrap(); |
||||
let bn: &str = sql_data.get("BACHNUMB").unwrap(); |
||||
let vi: &str = sql_data.get("VENDORID").unwrap(); |
||||
let pa: Decimal = sql_data.get("PRCHAMNT").unwrap(); |
||||
let dd: NaiveDateTime = sql_data.get("DOCDATE").unwrap(); |
||||
let dr: NaiveDateTime = sql_data.get("DEX_ROW_TS").unwrap(); |
||||
|
||||
Ok(PaymentData { |
||||
contract_number: cn.to_string(), |
||||
bach_num: bn.to_string(), |
||||
vendor_id: vi.to_string(), |
||||
purchase_amount: pa, |
||||
doc_date: dd, |
||||
dex_row_ts: dr, |
||||
}) |
||||
} |
||||
} |
||||
|
||||
pub trait FromSqlRow<T> { |
||||
fn from_sql_row(sql_data: Row) -> Result<T, BDynError>; |
||||
} |
||||
|
||||
Loading…
Reference in new issue