First commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
1745
Cargo.lock
generated
Normal file
1745
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "moy_nalog_rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = "0.4.42"
|
||||||
|
rand = "0.9.2"
|
||||||
|
reqwest = { version = "0.12.25", features = ["json"] }
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_json = "1.0.145"
|
||||||
|
tokio = { version = "1.48.0", features = ["full"] }
|
||||||
357
src/lib.rs
Normal file
357
src/lib.rs
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use rand::Rng;
|
||||||
|
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION, CACHE_CONTROL, CONTENT_TYPE, PRAGMA, ACCEPT_LANGUAGE};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LkNpdNalogApi {
|
||||||
|
api_url: String,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
response: Option<String>,
|
||||||
|
source_device_id: String,
|
||||||
|
token: Option<String>,
|
||||||
|
inn: Option<String>,
|
||||||
|
receipt_uuid: Option<String>,
|
||||||
|
receipt_url_print: Option<String>,
|
||||||
|
receipt_url_json: Option<String>,
|
||||||
|
error: bool,
|
||||||
|
error_message: Option<String>,
|
||||||
|
error_exception_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct AuthRequest {
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
#[serde(rename = "deviceInfo")]
|
||||||
|
device_info: DeviceInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct DeviceInfo {
|
||||||
|
#[serde(rename = "sourceDeviceId")]
|
||||||
|
source_device_id: String,
|
||||||
|
#[serde(rename = "sourceType")]
|
||||||
|
source_type: String,
|
||||||
|
#[serde(rename = "appVersion")]
|
||||||
|
app_version: String,
|
||||||
|
#[serde(rename = "metaDetails")]
|
||||||
|
meta_details: MetaDetails,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct MetaDetails {
|
||||||
|
#[serde(rename = "serAgent")]
|
||||||
|
user_agent: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AuthResponse {
|
||||||
|
token: Option<String>,
|
||||||
|
profile: Option<Profile>,
|
||||||
|
message: Option<String>,
|
||||||
|
code: Option<String>,
|
||||||
|
#[serde(rename = "exceptionMessage")]
|
||||||
|
exception_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Profile {
|
||||||
|
inn: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct CreateReceiptRequest {
|
||||||
|
#[serde(rename = "operationTime")]
|
||||||
|
operation_time: String,
|
||||||
|
#[serde(rename = "requestTime")]
|
||||||
|
request_time: String,
|
||||||
|
services: Vec<Service>,
|
||||||
|
#[serde(rename = "totalAmount")]
|
||||||
|
total_amount: f64,
|
||||||
|
client: Client,
|
||||||
|
#[serde(rename = "paymentType")]
|
||||||
|
payment_type: String,
|
||||||
|
#[serde(rename = "ignoreMaxTotalIncomeRestriction")]
|
||||||
|
ignore_max_total_income_restriction: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct Service {
|
||||||
|
name: String,
|
||||||
|
amount: f64,
|
||||||
|
quantity: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct Client {
|
||||||
|
#[serde(rename = "contactPhone")]
|
||||||
|
contact_phone: Option<String>,
|
||||||
|
#[serde(rename = "displayName")]
|
||||||
|
display_name: Option<String>,
|
||||||
|
inn: Option<String>,
|
||||||
|
#[serde(rename = "incomeType")]
|
||||||
|
income_type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct CancelReceiptRequest {
|
||||||
|
#[serde(rename = "operationTime")]
|
||||||
|
operation_time: String,
|
||||||
|
#[serde(rename = "requestTime")]
|
||||||
|
request_time: String,
|
||||||
|
comment: String,
|
||||||
|
#[serde(rename = "receiptUuid")]
|
||||||
|
receipt_uuid: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct ReceiptResponse {
|
||||||
|
#[serde(rename = "approvedReceiptUuid")]
|
||||||
|
approved_receipt_uuid: Option<String>,
|
||||||
|
#[serde(rename = "incomeInfo")]
|
||||||
|
income_info: Option<IncomeInfo>,
|
||||||
|
message: Option<String>,
|
||||||
|
code: Option<String>,
|
||||||
|
#[serde(rename = "exceptionMessage")]
|
||||||
|
exception_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct IncomeInfo {
|
||||||
|
#[serde(rename = "approvedReceiptUuid")]
|
||||||
|
approved_receipt_uuid: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CreateReceiptArgs {
|
||||||
|
pub name: String,
|
||||||
|
pub amount: f64,
|
||||||
|
pub client_contact_phone: Option<String>,
|
||||||
|
pub client_display_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CancelReceiptArgs {
|
||||||
|
pub receipt_uuid: String,
|
||||||
|
pub reason: CancelReason,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum CancelReason {
|
||||||
|
Cancel,
|
||||||
|
Refund,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LkNpdNalogApi {
|
||||||
|
pub fn new(username: String, password: String) -> Self {
|
||||||
|
let source_device_id = Self::create_device_id();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
api_url: "https://lknpd.nalog.ru/api/v1".to_string(),
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
response: None,
|
||||||
|
source_device_id,
|
||||||
|
token: None,
|
||||||
|
inn: None,
|
||||||
|
receipt_uuid: None,
|
||||||
|
receipt_url_print: None,
|
||||||
|
receipt_url_json: None,
|
||||||
|
error: false,
|
||||||
|
error_message: None,
|
||||||
|
error_exception_message: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_device_id() -> String {
|
||||||
|
const CHARSET: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
|
||||||
|
(0..20)
|
||||||
|
.map(|_| {
|
||||||
|
let idx = rng.random_range(0..CHARSET.len());
|
||||||
|
CHARSET[idx] as char
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_utc_time(&self) -> String {
|
||||||
|
let now: DateTime<Utc> = Utc::now();
|
||||||
|
now.to_rfc3339()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_receipt(&mut self, args: CreateReceiptArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.make_auth().await?;
|
||||||
|
|
||||||
|
if !self.error {
|
||||||
|
let payload = CreateReceiptRequest {
|
||||||
|
operation_time: self.get_utc_time(),
|
||||||
|
request_time: self.get_utc_time(),
|
||||||
|
services: vec![Service {
|
||||||
|
name: args.name,
|
||||||
|
amount: args.amount,
|
||||||
|
quantity: 1,
|
||||||
|
}],
|
||||||
|
total_amount: args.amount,
|
||||||
|
client: Client {
|
||||||
|
contact_phone: args.client_contact_phone,
|
||||||
|
display_name: args.client_display_name,
|
||||||
|
inn: None,
|
||||||
|
income_type: "FROM_INDIVIDUAL".to_string(),
|
||||||
|
},
|
||||||
|
payment_type: "CASH".to_string(),
|
||||||
|
ignore_max_total_income_restriction: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.make_query("createReceipt", &payload).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cancel_receipt(&mut self, args: CancelReceiptArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.make_auth().await?;
|
||||||
|
|
||||||
|
let comment = match args.reason {
|
||||||
|
CancelReason::Cancel => "Чек сформирован ошибочно",
|
||||||
|
CancelReason::Refund => "Возврат средств",
|
||||||
|
};
|
||||||
|
|
||||||
|
if !self.error {
|
||||||
|
let payload = CancelReceiptRequest {
|
||||||
|
operation_time: self.get_utc_time(),
|
||||||
|
request_time: self.get_utc_time(),
|
||||||
|
comment: comment.to_string(),
|
||||||
|
receipt_uuid: args.receipt_uuid,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.make_query("cancelReceipt", &payload).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn make_auth(&mut self) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let payload = AuthRequest {
|
||||||
|
username: self.username.clone(),
|
||||||
|
password: self.password.clone(),
|
||||||
|
device_info: DeviceInfo {
|
||||||
|
source_device_id: self.source_device_id.clone(),
|
||||||
|
source_type: "WEB".to_string(),
|
||||||
|
app_version: "1.0.0".to_string(),
|
||||||
|
meta_details: MetaDetails {
|
||||||
|
user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36".to_string(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
self.make_query("authLkfl", &payload).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn make_query<T: Serialize>(&mut self, method: &str, payload: &T) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let (endpoint, required_token) = match method {
|
||||||
|
"authLkfl" => ("/auth/lkfl", false),
|
||||||
|
"createReceipt" => ("/income", true),
|
||||||
|
"cancelReceipt" => ("/cancel", true),
|
||||||
|
_ => return Err("Unknown method".into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let url = format!("{}{}", self.api_url, endpoint);
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
||||||
|
headers.insert(ACCEPT, HeaderValue::from_static("application/json, text/plain, */*"));
|
||||||
|
headers.insert(ACCEPT_LANGUAGE, HeaderValue::from_static("ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7"));
|
||||||
|
headers.insert(CACHE_CONTROL, HeaderValue::from_static("no-cache"));
|
||||||
|
headers.insert(PRAGMA, HeaderValue::from_static("no-cache"));
|
||||||
|
|
||||||
|
if required_token {
|
||||||
|
if let Some(token) = &self.token {
|
||||||
|
headers.insert(
|
||||||
|
AUTHORIZATION,
|
||||||
|
HeaderValue::from_str(&format!("Bearer {}", token))?
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(&url)
|
||||||
|
.headers(headers)
|
||||||
|
.json(payload)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let response_text = response.text().await?;
|
||||||
|
self.response = Some(response_text.clone());
|
||||||
|
|
||||||
|
if method == "authLkfl" {
|
||||||
|
let auth_response: AuthResponse = serde_json::from_str(&response_text)?;
|
||||||
|
|
||||||
|
if auth_response.exception_message.is_some() || auth_response.code.is_some() {
|
||||||
|
self.error = true;
|
||||||
|
self.error_message = auth_response.message;
|
||||||
|
self.error_exception_message = auth_response.code.or(auth_response.exception_message);
|
||||||
|
} else {
|
||||||
|
self.error = false;
|
||||||
|
self.token = auth_response.token;
|
||||||
|
if let Some(profile) = auth_response.profile {
|
||||||
|
self.inn = Some(profile.inn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let receipt_response: ReceiptResponse = serde_json::from_str(&response_text)?;
|
||||||
|
|
||||||
|
if receipt_response.exception_message.is_some() || receipt_response.code.is_some() {
|
||||||
|
self.error = true;
|
||||||
|
self.error_message = receipt_response.message;
|
||||||
|
self.error_exception_message = receipt_response.code.or(receipt_response.exception_message);
|
||||||
|
} else {
|
||||||
|
self.error = false;
|
||||||
|
let rec_uuid = receipt_response.approved_receipt_uuid
|
||||||
|
.or_else(|| receipt_response.income_info
|
||||||
|
.and_then(|info| info.approved_receipt_uuid));
|
||||||
|
|
||||||
|
if let Some(uuid) = rec_uuid {
|
||||||
|
self.receipt_uuid = Some(uuid.clone());
|
||||||
|
if let Some(inn) = &self.inn {
|
||||||
|
self.receipt_url_print = Some(format!("{}/receipt/{}/{}/print", self.api_url, inn, uuid));
|
||||||
|
self.receipt_url_json = Some(format!("{}/receipt/{}/{}/json", self.api_url, inn, uuid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
pub fn error(&self) -> bool {
|
||||||
|
self.error
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error_message(&self) -> Option<&str> {
|
||||||
|
self.error_message.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error_exception_message(&self) -> Option<&str> {
|
||||||
|
self.error_exception_message.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receipt_uuid(&self) -> Option<&str> {
|
||||||
|
self.receipt_uuid.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receipt_url_print(&self) -> Option<&str> {
|
||||||
|
self.receipt_url_print.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn receipt_url_json(&self) -> Option<&str> {
|
||||||
|
self.receipt_url_json.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn response(&self) -> Option<&str> {
|
||||||
|
self.response.as_deref()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user