1use chrono::NaiveDate;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct License {
12 pub unique_system_identifier: i64,
14 pub call_sign: String,
16 pub licensee_name: String,
18 pub first_name: Option<String>,
20 pub middle_initial: Option<String>,
22 pub last_name: Option<String>,
24 pub status: char,
26 pub radio_service: String,
28 pub grant_date: Option<NaiveDate>,
30 pub expired_date: Option<NaiveDate>,
32 pub cancellation_date: Option<NaiveDate>,
34 pub frn: Option<String>,
36 pub street_address: Option<String>,
38 pub city: Option<String>,
40 pub state: Option<String>,
42 pub zip_code: Option<String>,
44 pub operator_class: Option<char>,
46 pub previous_call_sign: Option<String>,
48}
49
50impl License {
51 pub fn display_name(&self) -> String {
53 if let (Some(first), Some(last)) = (&self.first_name, &self.last_name) {
54 if let Some(mi) = &self.middle_initial {
55 format!("{} {} {}", first, mi, last)
56 } else {
57 format!("{} {}", first, last)
58 }
59 } else {
60 self.licensee_name.clone()
61 }
62 }
63
64 pub fn status_description(&self) -> &'static str {
66 match self.status {
67 'A' => "Active",
68 'C' => "Cancelled",
69 'E' => "Expired",
70 'L' => "Pending Legal Status",
71 'P' => "Parent Station Cancelled",
72 'T' => "Terminated",
73 'X' => "Term Pending",
74 _ => "Unknown",
75 }
76 }
77
78 pub fn is_active(&self) -> bool {
80 self.status == 'A'
81 }
82
83 pub fn operator_class_description(&self) -> Option<&'static str> {
85 self.operator_class.map(|c| match c {
86 'T' => "Technician",
87 'G' => "General",
88 'A' => "Advanced",
89 'E' => "Amateur Extra",
90 'N' => "Novice",
91 'P' => "Technician Plus",
92 _ => "Unknown",
93 })
94 }
95
96 pub fn get_field(&self, name: &str) -> Option<String> {
98 match name.to_lowercase().as_str() {
99 "call_sign" | "callsign" | "call" => Some(self.call_sign.clone()),
100 "name" | "licensee" | "entity_name" => Some(self.display_name()),
101 "first_name" | "first" => self.first_name.clone(),
102 "last_name" | "last" => self.last_name.clone(),
103 "middle_initial" | "mi" => self.middle_initial.clone(),
104 "status" | "license_status" => Some(self.status.to_string()),
105 "status_desc" | "status_description" => Some(self.status_description().to_string()),
106 "service" | "radio_service" => Some(self.radio_service.clone()),
107 "class" | "operator_class" => self.operator_class.map(|c| c.to_string()),
108 "class_desc" | "class_description" => {
109 self.operator_class_description().map(|s| s.to_string())
110 }
111 "city" => self.city.clone(),
112 "state" => self.state.clone(),
113 "zip" | "zip_code" => self.zip_code.clone(),
114 "location" => {
115 let city = self.city.as_deref().unwrap_or("");
116 let state = self.state.as_deref().unwrap_or("");
117 if city.is_empty() && state.is_empty() {
118 None
119 } else {
120 Some(format!("{}, {}", city, state))
121 }
122 }
123 "address" | "street_address" => self.street_address.clone(),
124 "frn" => self.frn.clone(),
125 "grant_date" | "granted" => self.grant_date.map(|d| d.to_string()),
126 "expired_date" | "expires" | "expiration" => self.expired_date.map(|d| d.to_string()),
127 "cancellation_date" | "cancelled" => self.cancellation_date.map(|d| d.to_string()),
128 "previous_call_sign" | "previous_call" => self.previous_call_sign.clone(),
129 "usi" | "unique_system_identifier" => Some(self.unique_system_identifier.to_string()),
130 _ => None,
131 }
132 }
133
134 pub fn field_names() -> &'static [&'static str] {
136 &[
137 "call_sign",
138 "name",
139 "first_name",
140 "last_name",
141 "status",
142 "status_desc",
143 "service",
144 "class",
145 "class_desc",
146 "city",
147 "state",
148 "zip",
149 "location",
150 "address",
151 "frn",
152 "grant_date",
153 "expired_date",
154 "cancellation_date",
155 "previous_call_sign",
156 "usi",
157 ]
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct Operator {
164 pub unique_system_identifier: i64,
166 pub call_sign: String,
168 pub operator_class: char,
170 pub group_code: Option<char>,
172 pub region_code: Option<i32>,
174 pub vanity_call: bool,
176 pub previous_operator_class: Option<char>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct LicenseStats {
183 pub total_licenses: u64,
185 pub active_licenses: u64,
187 pub expired_licenses: u64,
189 pub cancelled_licenses: u64,
191 pub by_service: Vec<(String, u64)>,
193 pub by_operator_class: Vec<(String, u64)>,
195 pub last_updated: Option<String>,
197 pub schema_version: i32,
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_license_display_name() {
207 let mut license = License {
208 unique_system_identifier: 1,
209 call_sign: "W1AW".to_string(),
210 licensee_name: "ARRL".to_string(),
211 first_name: Some("Test".to_string()),
212 middle_initial: Some("X".to_string()),
213 last_name: Some("User".to_string()),
214 status: 'A',
215 radio_service: "HA".to_string(),
216 grant_date: None,
217 expired_date: None,
218 cancellation_date: None,
219 frn: None,
220 street_address: None,
221 city: None,
222 state: None,
223 zip_code: None,
224 operator_class: Some('E'),
225 previous_call_sign: None,
226 };
227
228 assert_eq!(license.display_name(), "Test X User");
229
230 license.first_name = None;
231 assert_eq!(license.display_name(), "ARRL");
232 }
233
234 #[test]
235 fn test_operator_class_description() {
236 let license = License {
237 unique_system_identifier: 1,
238 call_sign: "W1AW".to_string(),
239 licensee_name: "Test".to_string(),
240 first_name: None,
241 middle_initial: None,
242 last_name: None,
243 status: 'A',
244 radio_service: "HA".to_string(),
245 grant_date: None,
246 expired_date: None,
247 cancellation_date: None,
248 frn: None,
249 street_address: None,
250 city: None,
251 state: None,
252 zip_code: None,
253 operator_class: Some('E'),
254 previous_call_sign: None,
255 };
256
257 assert_eq!(license.operator_class_description(), Some("Amateur Extra"));
258 assert!(license.is_active());
259 }
260
261 #[test]
262 fn test_status_description_all_variants() {
263 let mut license = License {
264 unique_system_identifier: 1,
265 call_sign: "W1AW".to_string(),
266 licensee_name: "Test".to_string(),
267 first_name: None,
268 middle_initial: None,
269 last_name: None,
270 status: 'A',
271 radio_service: "HA".to_string(),
272 grant_date: None,
273 expired_date: None,
274 cancellation_date: None,
275 frn: None,
276 street_address: None,
277 city: None,
278 state: None,
279 zip_code: None,
280 operator_class: None,
281 previous_call_sign: None,
282 };
283
284 assert_eq!(license.status_description(), "Active");
285
286 license.status = 'C';
287 assert_eq!(license.status_description(), "Cancelled");
288
289 license.status = 'E';
290 assert_eq!(license.status_description(), "Expired");
291
292 license.status = 'L';
293 assert_eq!(license.status_description(), "Pending Legal Status");
294
295 license.status = 'P';
296 assert_eq!(license.status_description(), "Parent Station Cancelled");
297
298 license.status = 'T';
299 assert_eq!(license.status_description(), "Terminated");
300
301 license.status = 'X';
302 assert_eq!(license.status_description(), "Term Pending");
303
304 license.status = 'Z';
305 assert_eq!(license.status_description(), "Unknown");
306 }
307
308 #[test]
309 fn test_operator_class_all_variants() {
310 let mut license = License {
311 unique_system_identifier: 1,
312 call_sign: "W1AW".to_string(),
313 licensee_name: "Test".to_string(),
314 first_name: None,
315 middle_initial: None,
316 last_name: None,
317 status: 'A',
318 radio_service: "HA".to_string(),
319 grant_date: None,
320 expired_date: None,
321 cancellation_date: None,
322 frn: None,
323 street_address: None,
324 city: None,
325 state: None,
326 zip_code: None,
327 operator_class: Some('T'),
328 previous_call_sign: None,
329 };
330
331 assert_eq!(license.operator_class_description(), Some("Technician"));
332
333 license.operator_class = Some('G');
334 assert_eq!(license.operator_class_description(), Some("General"));
335
336 license.operator_class = Some('A');
337 assert_eq!(license.operator_class_description(), Some("Advanced"));
338
339 license.operator_class = Some('N');
340 assert_eq!(license.operator_class_description(), Some("Novice"));
341
342 license.operator_class = Some('P');
343 assert_eq!(
344 license.operator_class_description(),
345 Some("Technician Plus")
346 );
347
348 license.operator_class = Some('Z');
349 assert_eq!(license.operator_class_description(), Some("Unknown"));
350
351 license.operator_class = None;
352 assert_eq!(license.operator_class_description(), None);
353 }
354
355 #[test]
356 fn test_display_name_without_middle_initial() {
357 let license = License {
358 unique_system_identifier: 1,
359 call_sign: "W1AW".to_string(),
360 licensee_name: "ARRL".to_string(),
361 first_name: Some("John".to_string()),
362 middle_initial: None,
363 last_name: Some("Doe".to_string()),
364 status: 'A',
365 radio_service: "HA".to_string(),
366 grant_date: None,
367 expired_date: None,
368 cancellation_date: None,
369 frn: None,
370 street_address: None,
371 city: None,
372 state: None,
373 zip_code: None,
374 operator_class: None,
375 previous_call_sign: None,
376 };
377
378 assert_eq!(license.display_name(), "John Doe");
379 }
380
381 #[test]
382 fn test_get_field_basic() {
383 let license = License {
384 unique_system_identifier: 12345,
385 call_sign: "W1AW".to_string(),
386 licensee_name: "ARRL".to_string(),
387 first_name: Some("John".to_string()),
388 middle_initial: Some("Q".to_string()),
389 last_name: Some("Public".to_string()),
390 status: 'A',
391 radio_service: "HA".to_string(),
392 grant_date: Some(NaiveDate::from_ymd_opt(2020, 1, 15).unwrap()),
393 expired_date: Some(NaiveDate::from_ymd_opt(2030, 1, 15).unwrap()),
394 cancellation_date: None,
395 frn: Some("0012345678".to_string()),
396 street_address: Some("123 Main St".to_string()),
397 city: Some("Newington".to_string()),
398 state: Some("CT".to_string()),
399 zip_code: Some("06111".to_string()),
400 operator_class: Some('E'),
401 previous_call_sign: Some("N1XYZ".to_string()),
402 };
403
404 assert_eq!(license.get_field("call_sign"), Some("W1AW".to_string()));
406 assert_eq!(license.get_field("callsign"), Some("W1AW".to_string()));
407 assert_eq!(license.get_field("call"), Some("W1AW".to_string()));
408
409 assert_eq!(license.get_field("name"), Some("John Q Public".to_string()));
410 assert_eq!(
411 license.get_field("licensee"),
412 Some("John Q Public".to_string())
413 );
414
415 assert_eq!(license.get_field("first_name"), Some("John".to_string()));
416 assert_eq!(license.get_field("first"), Some("John".to_string()));
417
418 assert_eq!(license.get_field("last_name"), Some("Public".to_string()));
419 assert_eq!(license.get_field("last"), Some("Public".to_string()));
420
421 assert_eq!(license.get_field("middle_initial"), Some("Q".to_string()));
422 assert_eq!(license.get_field("mi"), Some("Q".to_string()));
423
424 assert_eq!(license.get_field("status"), Some("A".to_string()));
425 assert_eq!(license.get_field("status_desc"), Some("Active".to_string()));
426
427 assert_eq!(license.get_field("service"), Some("HA".to_string()));
428 assert_eq!(license.get_field("radio_service"), Some("HA".to_string()));
429
430 assert_eq!(license.get_field("class"), Some("E".to_string()));
431 assert_eq!(
432 license.get_field("class_desc"),
433 Some("Amateur Extra".to_string())
434 );
435
436 assert_eq!(license.get_field("city"), Some("Newington".to_string()));
437 assert_eq!(license.get_field("state"), Some("CT".to_string()));
438 assert_eq!(license.get_field("zip"), Some("06111".to_string()));
439 assert_eq!(license.get_field("zip_code"), Some("06111".to_string()));
440
441 assert_eq!(
442 license.get_field("location"),
443 Some("Newington, CT".to_string())
444 );
445
446 assert_eq!(
447 license.get_field("address"),
448 Some("123 Main St".to_string())
449 );
450 assert_eq!(
451 license.get_field("street_address"),
452 Some("123 Main St".to_string())
453 );
454
455 assert_eq!(license.get_field("frn"), Some("0012345678".to_string()));
456
457 assert_eq!(
458 license.get_field("grant_date"),
459 Some("2020-01-15".to_string())
460 );
461 assert_eq!(license.get_field("granted"), Some("2020-01-15".to_string()));
462
463 assert_eq!(
464 license.get_field("expired_date"),
465 Some("2030-01-15".to_string())
466 );
467 assert_eq!(license.get_field("expires"), Some("2030-01-15".to_string()));
468
469 assert_eq!(license.get_field("cancellation_date"), None);
470 assert_eq!(license.get_field("cancelled"), None);
471
472 assert_eq!(
473 license.get_field("previous_call_sign"),
474 Some("N1XYZ".to_string())
475 );
476 assert_eq!(
477 license.get_field("previous_call"),
478 Some("N1XYZ".to_string())
479 );
480
481 assert_eq!(license.get_field("usi"), Some("12345".to_string()));
482 assert_eq!(
483 license.get_field("unique_system_identifier"),
484 Some("12345".to_string())
485 );
486
487 assert_eq!(license.get_field("unknown_field"), None);
489 }
490
491 #[test]
492 fn test_get_field_location_empty() {
493 let license = License {
494 unique_system_identifier: 1,
495 call_sign: "W1AW".to_string(),
496 licensee_name: "Test".to_string(),
497 first_name: None,
498 middle_initial: None,
499 last_name: None,
500 status: 'A',
501 radio_service: "HA".to_string(),
502 grant_date: None,
503 expired_date: None,
504 cancellation_date: None,
505 frn: None,
506 street_address: None,
507 city: None,
508 state: None,
509 zip_code: None,
510 operator_class: None,
511 previous_call_sign: None,
512 };
513
514 assert_eq!(license.get_field("location"), None);
516 }
517
518 #[test]
519 fn test_get_field_no_operator_class() {
520 let license = License {
521 unique_system_identifier: 1,
522 call_sign: "W1AW".to_string(),
523 licensee_name: "Test".to_string(),
524 first_name: None,
525 middle_initial: None,
526 last_name: None,
527 status: 'A',
528 radio_service: "ZA".to_string(), grant_date: None,
530 expired_date: None,
531 cancellation_date: None,
532 frn: None,
533 street_address: None,
534 city: None,
535 state: None,
536 zip_code: None,
537 operator_class: None,
538 previous_call_sign: None,
539 };
540
541 assert_eq!(license.get_field("class"), None);
542 assert_eq!(license.get_field("class_desc"), None);
543 }
544
545 #[test]
546 fn test_field_names() {
547 let names = License::field_names();
548 assert!(names.contains(&"call_sign"));
549 assert!(names.contains(&"name"));
550 assert!(names.contains(&"status"));
551 assert!(names.contains(&"city"));
552 assert!(names.contains(&"state"));
553 assert!(names.contains(&"frn"));
554 assert!(names.contains(&"grant_date"));
555 assert!(names.len() >= 15);
556 }
557
558 #[test]
559 fn test_is_active() {
560 let mut license = License {
561 unique_system_identifier: 1,
562 call_sign: "W1AW".to_string(),
563 licensee_name: "Test".to_string(),
564 first_name: None,
565 middle_initial: None,
566 last_name: None,
567 status: 'A',
568 radio_service: "HA".to_string(),
569 grant_date: None,
570 expired_date: None,
571 cancellation_date: None,
572 frn: None,
573 street_address: None,
574 city: None,
575 state: None,
576 zip_code: None,
577 operator_class: None,
578 previous_call_sign: None,
579 };
580
581 assert!(license.is_active());
582
583 license.status = 'E';
584 assert!(!license.is_active());
585
586 license.status = 'C';
587 assert!(!license.is_active());
588 }
589
590 #[test]
591 fn test_get_field_cancellation_date_with_value() {
592 let license = License {
595 unique_system_identifier: 1,
596 call_sign: "W1AW".to_string(),
597 licensee_name: "Test".to_string(),
598 first_name: None,
599 middle_initial: None,
600 last_name: None,
601 status: 'C',
602 radio_service: "HA".to_string(),
603 grant_date: None,
604 expired_date: None,
605 cancellation_date: Some(NaiveDate::from_ymd_opt(2023, 6, 15).unwrap()),
606 frn: None,
607 street_address: None,
608 city: None,
609 state: None,
610 zip_code: None,
611 operator_class: None,
612 previous_call_sign: None,
613 };
614
615 assert_eq!(
616 license.get_field("cancellation_date"),
617 Some("2023-06-15".to_string())
618 );
619 assert_eq!(
620 license.get_field("cancelled"),
621 Some("2023-06-15".to_string())
622 );
623 }
624
625 #[test]
626 fn test_get_field_location_partial() {
627 let mut license = License {
629 unique_system_identifier: 1,
630 call_sign: "W1AW".to_string(),
631 licensee_name: "Test".to_string(),
632 first_name: None,
633 middle_initial: None,
634 last_name: None,
635 status: 'A',
636 radio_service: "HA".to_string(),
637 grant_date: None,
638 expired_date: None,
639 cancellation_date: None,
640 frn: None,
641 street_address: None,
642 city: Some("Boston".to_string()),
643 state: None,
644 zip_code: None,
645 operator_class: None,
646 previous_call_sign: None,
647 };
648
649 assert_eq!(license.get_field("location"), Some("Boston, ".to_string()));
651
652 license.city = None;
654 license.state = Some("MA".to_string());
655 assert_eq!(license.get_field("location"), Some(", MA".to_string()));
656 }
657
658 #[test]
659 fn test_get_field_expiration_alias() {
660 let license = License {
662 unique_system_identifier: 1,
663 call_sign: "W1AW".to_string(),
664 licensee_name: "Test".to_string(),
665 first_name: None,
666 middle_initial: None,
667 last_name: None,
668 status: 'A',
669 radio_service: "HA".to_string(),
670 grant_date: None,
671 expired_date: Some(NaiveDate::from_ymd_opt(2030, 12, 31).unwrap()),
672 cancellation_date: None,
673 frn: None,
674 street_address: None,
675 city: None,
676 state: None,
677 zip_code: None,
678 operator_class: None,
679 previous_call_sign: None,
680 };
681
682 assert_eq!(
683 license.get_field("expiration"),
684 Some("2030-12-31".to_string())
685 );
686 }
687
688 #[test]
689 fn test_get_field_entity_name_alias() {
690 let license = License {
692 unique_system_identifier: 1,
693 call_sign: "W1AW".to_string(),
694 licensee_name: "ARRL HQ Station".to_string(),
695 first_name: None,
696 middle_initial: None,
697 last_name: None,
698 status: 'A',
699 radio_service: "HA".to_string(),
700 grant_date: None,
701 expired_date: None,
702 cancellation_date: None,
703 frn: None,
704 street_address: None,
705 city: None,
706 state: None,
707 zip_code: None,
708 operator_class: None,
709 previous_call_sign: None,
710 };
711
712 assert_eq!(
714 license.get_field("entity_name"),
715 Some("ARRL HQ Station".to_string())
716 );
717 }
718
719 #[test]
720 fn test_get_field_license_status_alias() {
721 let license = License {
723 unique_system_identifier: 1,
724 call_sign: "W1AW".to_string(),
725 licensee_name: "Test".to_string(),
726 first_name: None,
727 middle_initial: None,
728 last_name: None,
729 status: 'E',
730 radio_service: "HA".to_string(),
731 grant_date: None,
732 expired_date: None,
733 cancellation_date: None,
734 frn: None,
735 street_address: None,
736 city: None,
737 state: None,
738 zip_code: None,
739 operator_class: None,
740 previous_call_sign: None,
741 };
742
743 assert_eq!(license.get_field("license_status"), Some("E".to_string()));
744 }
745}