1use crate::wire::OperationCode;
12use serde::{Deserialize, Serialize};
13
14pub mod cash;
16pub mod config;
18pub mod directory;
20pub mod misc;
22pub mod receipt;
24pub mod reports;
26pub mod session;
28
29pub use cash::CashInOutRequest;
30pub use config::{SetupHeaderFooterRequest, SetupHeaderLogoRequest, TextAlign, TextLine};
31pub use directory::{
32 DepartmentInfo, ListOpsAndDepsRequest, ListOpsAndDepsResponse, OperatorInfo, TaxationKind,
33};
34pub use misc::{
35 DateTimeRequest, DateTimeResponse, HdmTimeSyncRequest, PaymentSystemEntry,
36 PaymentSystemsListRequest, PaymentSystemsListResponse, ReceiptSampleRequest,
37 SingleEmarkRequest,
38};
39pub use receipt::{
40 DiscountKind, GetReturnableReceiptRequest, PrintLastReceiptRequest, PrintMode,
41 PrintReceiptRequest, PrintReturnReceiptRequest, ReceiptItem, ReceiptResponse, ReturnItem,
42 ReturnReceiptResponse, ReturnableReceiptItem, ReturnableReceiptResponse,
43};
44pub use reports::{FiscalReportKind, FiscalReportRequest, ReportFilter};
45pub use session::{OperatorLoginRequest, OperatorLoginResponse, OperatorLogoutRequest};
46
47#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize)]
52#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
53#[serde(default)]
54pub struct EmptyResponse {}
55
56pub trait Operation: Serialize {
59 const CODE: OperationCode;
61 type Response: for<'de> Deserialize<'de>;
63 const USES_PASSWORD_KEY: bool = false;
66 const RESPONSE_IS_SECRET: bool = false;
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use crate::wire::OperationCode;
76 use rust_decimal::Decimal;
77
78 #[test]
80 fn print_mode_wire_encoding() {
81 assert_eq!(serde_json::to_string(&PrintMode::Simple).unwrap(), "1");
82 assert_eq!(serde_json::to_string(&PrintMode::Products).unwrap(), "2");
83 assert_eq!(serde_json::to_string(&PrintMode::Prepayment).unwrap(), "3");
84 }
85
86 #[test]
88 fn discount_kind_wire_encoding() {
89 assert_eq!(serde_json::to_string(&DiscountKind::Percent).unwrap(), "1");
90 assert_eq!(
91 serde_json::to_string(&DiscountKind::UnitPriceReduction).unwrap(),
92 "2"
93 );
94 assert_eq!(
95 serde_json::to_string(&DiscountKind::LineTotalReduction).unwrap(),
96 "4"
97 );
98 assert_eq!(
99 serde_json::to_string(&DiscountKind::AdditionalPercent).unwrap(),
100 "8"
101 );
102 assert_eq!(
103 serde_json::to_string(&DiscountKind::AdditionalMonetary).unwrap(),
104 "16"
105 );
106 }
107
108 #[test]
110 fn text_align_wire_encoding() {
111 assert_eq!(serde_json::to_string(&TextAlign::Left).unwrap(), "1");
112 assert_eq!(serde_json::to_string(&TextAlign::Centered).unwrap(), "2");
113 assert_eq!(serde_json::to_string(&TextAlign::Right).unwrap(), "3");
114 }
115
116 #[test]
118 fn fiscal_report_kind_wire_encoding() {
119 assert_eq!(serde_json::to_string(&FiscalReportKind::X).unwrap(), "1");
120 assert_eq!(serde_json::to_string(&FiscalReportKind::Z).unwrap(), "2");
121 }
122
123 #[test]
125 fn taxation_kind_preserves_unknown_codes() {
126 let unknown = TaxationKind::Unknown(99);
127 let json = serde_json::to_string(&unknown).unwrap();
128 assert_eq!(json, "99");
129 let parsed: TaxationKind = serde_json::from_str(&json).unwrap();
130 assert_eq!(parsed, TaxationKind::Unknown(99));
131 }
132
133 #[test]
135 fn taxation_kind_named_variants_round_trip() {
136 for &kind in &[
137 TaxationKind::VatTaxable,
138 TaxationKind::NotVatTaxable,
139 TaxationKind::TurnoverTax,
140 TaxationKind::ProductionLicensee,
141 TaxationKind::Patented,
142 TaxationKind::FamilyBusiness,
143 TaxationKind::MicroBusiness,
144 ] {
145 let json = serde_json::to_string(&kind).unwrap();
146 let parsed: TaxationKind = serde_json::from_str(&json).unwrap();
147 assert_eq!(parsed, kind);
148 }
149 }
150
151 #[test]
153 fn partner_tin_serialises_as_null_when_absent() {
154 let req = PrintReceiptRequest {
155 mode: PrintMode::Simple,
156 paid_amount: Decimal::ZERO,
157 paid_amount_card: Decimal::ZERO,
158 partial_amount: Decimal::ZERO,
159 pre_payment_amount: Decimal::ZERO,
160 dep: Some(1),
161 partner_tin: None,
162 use_ext_pos: false,
163 payment_system: None,
164 rrn: None,
165 terminal_id: None,
166 e_marks: vec![],
167 items: vec![],
168 };
169 let json = serde_json::to_string(&req).unwrap();
170 assert!(
171 json.contains(r#""partnerTin":null"#),
172 "partnerTin should serialise as explicit null, got {json}"
173 );
174 }
175
176 #[test]
178 fn print_receipt_request_uses_spec_wire_keys() {
179 let req = PrintReceiptRequest {
180 mode: PrintMode::Products,
181 paid_amount: Decimal::from(2200),
182 paid_amount_card: Decimal::from(10),
183 partial_amount: Decimal::ZERO,
184 pre_payment_amount: Decimal::ZERO,
185 dep: None,
186 partner_tin: None,
187 use_ext_pos: true,
188 payment_system: None,
189 rrn: Some("123456789012".to_owned()),
190 terminal_id: Some("12345678".to_owned()),
191 e_marks: vec!["xxx".to_owned()],
192 items: vec![ReceiptItem {
193 dep: 1,
194 qty: Decimal::from(3),
195 price: Decimal::from(1000),
196 product_code: "001".to_owned(),
197 product_name: "Pepsi".to_owned(),
198 adg_code: Some("0104".to_owned()),
199 unit: "litr".to_owned(),
200 discount: Some(Decimal::from(10)),
201 discount_kind: Some(DiscountKind::Percent),
202 additional_discount: Some(Decimal::from(500)),
203 additional_discount_kind: Some(DiscountKind::AdditionalMonetary),
204 }],
205 };
206 let json = serde_json::to_string(&req).unwrap();
207 for key in &[
208 r#""paidAmount":2200.0"#,
209 r#""paidAmountCard":10.0"#,
210 r#""partialAmount":0.0"#,
211 r#""prePaymentAmount":0.0"#,
212 r#""useExtPOS":true"#,
213 r#""terminalId":"12345678""#,
214 r#""eMarks":["xxx"]"#,
215 r#""productCode":"001""#,
216 r#""productName":"Pepsi""#,
217 r#""adgCode":"0104""#,
218 r#""discountType":1"#,
219 r#""additionalDiscount":500.0"#,
220 r#""additionalDiscountType":16"#,
221 ] {
222 assert!(json.contains(key), "missing key {key} in {json}");
223 }
224 for forbidden in &["paid_amount", "use_ext_pos", "terminal_id", "product_code"] {
226 assert!(!json.contains(forbidden), "unexpected snake_case in {json}");
227 }
228 }
229
230 #[test]
232 fn receipt_response_parses_spec_example() {
233 let json = r#"{
234 "rseq": 179,
235 "crn": "31008940",
236 "sn": "Q80414503833",
237 "tin": "00000019",
238 "taxpayer": "LUSARD",
239 "address": "Yerevan",
240 "time": 1490190340000,
241 "fiscal": "68287355",
242 "lottery": "00000002",
243 "prize": 0,
244 "total": 3000.0,
245 "change": 0.0,
246 "emarksCount": "1",
247 "verificationNumber": "128503",
248 "qr": "TIN:00000019..."
249 }"#;
250 let parsed: ReceiptResponse = serde_json::from_str(json).unwrap();
251 assert_eq!(parsed.rseq, 179);
252 assert_eq!(parsed.fiscal, "68287355");
253 assert_eq!(parsed.qr.as_deref(), Some("TIN:00000019..."));
254 assert_eq!(parsed.verification_number.as_deref(), Some("128503"));
255 }
256
257 #[test]
259 fn receipt_response_parses_v05_response_without_post_v05_fields() {
260 let json = r#"{
261 "rseq": 100,
262 "crn": "0001",
263 "sn": "SN",
264 "tin": "TIN",
265 "taxpayer": "TP",
266 "address": "Addr",
267 "time": 1,
268 "fiscal": "FN",
269 "lottery": "L",
270 "prize": 0,
271 "total": 50.0,
272 "change": 0.0
273 }"#;
274 let parsed: ReceiptResponse = serde_json::from_str(json).unwrap();
275 assert_eq!(parsed.rseq, 100);
276 assert!(parsed.qr.is_none());
277 assert!(parsed.verification_number.is_none());
278 assert!(parsed.emarks_count.is_none());
279 }
280
281 #[test]
286 fn operation_couples_request_to_code_and_response() {
287 const { assert!(OperatorLoginRequest::USES_PASSWORD_KEY) };
288 const { assert!(!PrintReceiptRequest::USES_PASSWORD_KEY) };
289 const { assert!(ListOpsAndDepsRequest::USES_PASSWORD_KEY) };
290 const { assert!(!PaymentSystemsListRequest::USES_PASSWORD_KEY) };
291
292 assert_eq!(OperatorLoginRequest::CODE, OperationCode::OperatorLogin);
293 assert_eq!(PrintReceiptRequest::CODE, OperationCode::PrintReceipt);
294 assert_eq!(ListOpsAndDepsRequest::CODE, OperationCode::ListOpsAndDeps);
295 assert_eq!(
296 PaymentSystemsListRequest::CODE,
297 OperationCode::PaymentSystemsList
298 );
299 }
300
301 #[test]
306 fn return_flow_codes_match_spec_table() {
307 assert_eq!(PrintReturnReceiptRequest::CODE as u8, 6);
308 assert_eq!(GetReturnableReceiptRequest::CODE as u8, 10);
309 }
310
311 #[test]
313 fn empty_response_accepts_empty_and_unknown_fields() {
314 let _: EmptyResponse = serde_json::from_str("{}").unwrap();
315 let _: EmptyResponse = serde_json::from_str(r#"{"x":1,"y":"z"}"#).unwrap();
316 }
317
318 #[test]
320 fn payment_systems_list_parses_spec_example() {
321 let json = r#"{
322 "PaymentSystems": [
323 { "code": 1, "name": "Card Payment" },
324 { "code": 13, "name": "IDRAM" }
325 ]
326 }"#;
327 let parsed: PaymentSystemsListResponse = serde_json::from_str(json).unwrap();
328 assert_eq!(parsed.payment_systems.len(), 2);
329 assert_eq!(parsed.payment_systems[0].code, 1);
330 assert_eq!(parsed.payment_systems[1].code, 13);
331 assert_eq!(parsed.payment_systems[1].name, "IDRAM");
332 }
333
334 #[test]
338 fn returnable_receipt_parses_spec_code_block_7() {
339 let json = r#"{
340 "time": 1450260000, "type": 0, "ref": 0, "cid": 3,
341 "ta": 3000, "cash": 1000, "card": 1000, "ppu": 0, "ppa": 1000,
342 "saleType": 0, "pTin": "12345678",
343 "eMarks": ["aaa", "bbb"],
344 "totals": [
345 { "gc": "001", "gn": "Product1", "qty": 1, "p": 1000, "mu": "x",
346 "rpid": 0, "dsc": null, "adsc": null, "dsct": null,
347 "did": 1, "dt": 16.67, "dtm": 1, "t": 833.33, "tt": 1000.00 }
348 ]
349 }"#;
350 let r: ReturnableReceiptResponse = serde_json::from_str(json).unwrap();
351 assert_eq!(r.cid, Some(3));
353 assert_eq!(r.sale_type, Some(0));
354 assert_eq!(r.kind, Some(0));
355 assert_eq!(r.returned_receipt, Some(0));
356 assert_eq!(r.ta, Some(Decimal::from(3000)));
358 assert_eq!(r.partner_tin.as_deref(), Some("12345678"));
359 assert_eq!(r.e_marks.len(), 2);
360 assert_eq!(r.rseq, None);
362 assert_eq!(r.sub_type, None);
363 assert_eq!(r.returned_crn, None);
364 assert_eq!(r.totals.len(), 1);
366 let [item] = r.totals.as_slice() else {
367 panic!("expected one item")
368 };
369 assert_eq!(item.rpid, Some(0));
370 assert_eq!(item.qty, Some(Decimal::from(1)));
371 assert_eq!(item.discount, None); assert_eq!(item.did, Some(1));
373 assert_eq!(item.vat_amount, Some(Decimal::new(1667, 2)));
374 }
375
376 #[test]
379 fn returnable_receipt_parses_empty_object() {
380 let r: ReturnableReceiptResponse = serde_json::from_str("{}").unwrap();
381 assert!(r.rseq.is_none() && r.cid.is_none() && r.ta.is_none());
382 assert!(r.e_marks.is_empty() && r.totals.is_empty());
383 }
384
385 #[test]
389 fn print_mode_deserialises_from_wire_value() {
390 assert_eq!(
391 serde_json::from_str::<PrintMode>("1").unwrap(),
392 PrintMode::Simple
393 );
394 assert_eq!(
395 serde_json::from_str::<PrintMode>("3").unwrap(),
396 PrintMode::Prepayment
397 );
398 assert!(serde_json::from_str::<PrintMode>("9").is_err());
399 }
400
401 #[test]
403 fn fiscal_report_kind_deserialises_from_wire_value() {
404 assert_eq!(
405 serde_json::from_str::<FiscalReportKind>("1").unwrap(),
406 FiscalReportKind::X
407 );
408 assert_eq!(
409 serde_json::from_str::<FiscalReportKind>("2").unwrap(),
410 FiscalReportKind::Z
411 );
412 assert!(serde_json::from_str::<FiscalReportKind>("0").is_err());
413 }
414
415 #[test]
417 fn print_receipt_request_deserialises_with_defaults() {
418 let json = r#"{
419 "mode": 1, "paidAmount": 1000.0, "paidAmountCard": 0.0,
420 "partialAmount": 0.0, "prePaymentAmount": 0.0,
421 "useExtPOS": false, "dep": 1
422 }"#;
423 let req: PrintReceiptRequest = serde_json::from_str(json).unwrap();
424 assert_eq!(req.mode, PrintMode::Simple);
425 assert_eq!(req.paid_amount, Decimal::from(1000));
426 assert_eq!(req.dep, Some(1));
427 assert!(req.partner_tin.is_none());
428 assert!(req.e_marks.is_empty());
429 assert!(req.items.is_empty());
430 }
431
432 #[test]
435 fn fiscal_report_request_filter_round_trips() {
436 let with_filter: FiscalReportRequest =
437 serde_json::from_str(r#"{"reportType":1,"deptId":5,"startDate":0,"endDate":0}"#)
438 .unwrap();
439 assert_eq!(with_filter.kind, FiscalReportKind::X);
440 assert_eq!(with_filter.filter, Some(ReportFilter::Department(5)));
441
442 let no_filter: FiscalReportRequest =
443 serde_json::from_str(r#"{"reportType":2,"startDate":0,"endDate":0}"#).unwrap();
444 assert_eq!(no_filter.kind, FiscalReportKind::Z);
445 assert_eq!(no_filter.filter, None);
446
447 let original = FiscalReportRequest {
449 kind: FiscalReportKind::X,
450 filter: Some(ReportFilter::Cashier(7)),
451 start_date: 0,
452 end_date: 0,
453 };
454 let json = serde_json::to_string(&original).unwrap();
455 let back: FiscalReportRequest = serde_json::from_str(&json).unwrap();
456 assert_eq!(back.filter, Some(ReportFilter::Cashier(7)));
457 }
458}