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