First commit

This commit is contained in:
2025-12-11 23:59:56 +05:00
commit 9b67af7a89
4 changed files with 2115 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

1745
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

12
Cargo.toml Normal file
View 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
View 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()
}
}