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