1use bitwarden_vault::CardBrand;
6use chrono::Month;
7use credential_exchange_format::{Credential, CreditCardCredential, EditableFieldYearMonth};
8use num_traits::FromPrimitive;
9
10use crate::{cxf::editable_field::create_field, Card, Field};
11
12impl From<Card> for Vec<Credential> {
13 fn from(value: Card) -> Self {
14 let expiry_date = match (value.exp_year, value.exp_month) {
15 (Some(year), Some(month)) => {
16 let year_parsed = year.parse().ok();
17 let numeric_month: Option<u32> = month.parse().ok();
18 let month_parsed = numeric_month.and_then(Month::from_u32);
19 match (year_parsed, month_parsed) {
20 (Some(year), Some(month)) => {
21 Some(EditableFieldYearMonth { year, month }.into())
22 }
23 _ => None,
24 }
25 }
26 _ => None,
27 };
28
29 vec![Credential::CreditCard(Box::new(CreditCardCredential {
30 number: value.number.map(|v| v.into()),
31 full_name: value.cardholder_name.map(|v| v.into()),
32 card_type: value.brand.map(|v| v.into()),
33 verification_number: value.code.map(|v| v.into()),
34 pin: None,
35 expiry_date,
36 valid_from: None,
37 }))]
38 }
39}
40
41impl From<&CreditCardCredential> for Card {
42 fn from(value: &CreditCardCredential) -> Self {
43 Card {
44 cardholder_name: value.full_name.clone().map(|v| v.into()),
45 exp_month: value
46 .expiry_date
47 .as_ref()
48 .map(|v| v.value.month.number_from_month().to_string()),
49 exp_year: value.expiry_date.as_ref().map(|v| v.value.year.to_string()),
50 code: value.verification_number.clone().map(|v| v.into()),
51 brand: value
52 .card_type
53 .as_ref()
54 .and_then(|brand| sanitize_brand(&brand.value.0)),
55 number: value.number.clone().map(|v| v.into()),
56 }
57 }
58}
59
60pub(super) fn to_card(credential: &CreditCardCredential) -> (Card, Vec<Field>) {
61 let card = credential.into();
62
63 let fields = [
64 credential.pin.as_ref().map(|v| create_field("PIN", v)),
65 credential
66 .valid_from
67 .as_ref()
68 .map(|v| create_field("Valid From", v)),
69 ]
70 .into_iter()
71 .flatten()
72 .collect();
73
74 (card, fields)
75}
76
77fn sanitize_brand(value: &str) -> Option<String> {
85 match value.to_lowercase().replace(" ", "").as_str() {
86 "visa" => Some(CardBrand::Visa),
87 "mastercard" => Some(CardBrand::Mastercard),
88 "amex" | "americanexpress" => Some(CardBrand::Amex),
89 "discover" => Some(CardBrand::Discover),
90 "dinersclub" => Some(CardBrand::DinersClub),
91 "jcb" => Some(CardBrand::Jcb),
92 "maestro" => Some(CardBrand::Maestro),
93 "unionpay" => Some(CardBrand::UnionPay),
94 "rupay" => Some(CardBrand::RuPay),
95 _ => None,
96 }
97 .and_then(|brand| serde_json::to_value(&brand).ok())
98 .and_then(|v| v.as_str().map(|s| s.to_string()))
99}
100
101#[cfg(test)]
102mod tests {
103 use bitwarden_vault::FieldType;
104 use chrono::Month;
105 use credential_exchange_format::EditableFieldYearMonth;
106
107 use super::*;
108
109 #[test]
110 fn test_sanitize_brand() {
111 assert_eq!(sanitize_brand("Visa"), Some("Visa".to_string()));
112 assert_eq!(sanitize_brand(" visa "), Some("Visa".to_string()));
113 assert_eq!(sanitize_brand("MasterCard"), Some("Mastercard".to_string()));
114 assert_eq!(sanitize_brand("amex"), Some("Amex".to_string()));
115 assert_eq!(sanitize_brand("American Express"), Some("Amex".to_string()));
116 assert_eq!(
117 sanitize_brand("DinersClub"),
118 Some("Diners Club".to_string())
119 );
120 assert_eq!(sanitize_brand("j c b"), Some("JCB".to_string()));
121 assert_eq!(sanitize_brand("Some unknown"), None);
122 }
123
124 #[test]
125 fn test_card_to_credentials() {
126 let card = Card {
127 cardholder_name: Some("John Doe".to_string()),
128 exp_month: Some("12".to_string()),
129 exp_year: Some("2025".to_string()),
130 code: Some("123".to_string()),
131 brand: Some("Visa".to_string()),
132 number: Some("4111111111111111".to_string()),
133 };
134
135 let credentials: Vec<Credential> = card.into();
136 assert_eq!(credentials.len(), 1);
137
138 if let Credential::CreditCard(credit_card) = &credentials[0] {
139 assert_eq!(credit_card.full_name.as_ref().unwrap().value.0, "John Doe");
140 assert_eq!(
141 credit_card.expiry_date.as_ref().unwrap().value,
142 EditableFieldYearMonth {
143 year: 2025,
144 month: Month::December
145 }
146 );
147 assert_eq!(
148 credit_card.verification_number.as_ref().unwrap().value.0,
149 "123".to_string()
150 );
151 assert_eq!(
152 credit_card.card_type.as_ref().unwrap().value.0,
153 "Visa".to_string()
154 );
155 assert_eq!(
156 credit_card.number.as_ref().unwrap().value.0,
157 "4111111111111111"
158 );
159 } else {
160 panic!("Expected CreditCardCredential");
161 }
162 }
163
164 #[test]
165 fn test_credit_card_credential_to_card() {
166 let credit_card = CreditCardCredential {
167 number: Some("4111111111111111".to_string().into()),
168 full_name: Some("John Doe".to_string().into()),
169 card_type: Some("Visa".to_string().into()),
170 verification_number: Some("123".to_string().into()),
171 pin: Some("4567".to_string().into()),
172 expiry_date: Some(
173 EditableFieldYearMonth {
174 year: 2025,
175 month: Month::December,
176 }
177 .into(),
178 ),
179 valid_from: Some(
180 EditableFieldYearMonth {
181 year: 2024,
182 month: Month::January,
183 }
184 .into(),
185 ),
186 };
187
188 let (card, fields) = to_card(&credit_card);
189 assert_eq!(card.cardholder_name, Some("John Doe".to_string()));
190 assert_eq!(card.exp_month, Some("12".to_string()));
191 assert_eq!(card.exp_year, Some("2025".to_string()));
192 assert_eq!(card.code, Some("123".to_string()));
193 assert_eq!(card.brand, Some("Visa".to_string()));
194 assert_eq!(card.number, Some("4111111111111111".to_string()));
195
196 assert_eq!(
197 fields,
198 vec![
199 Field {
200 name: Some("PIN".to_string()),
201 value: Some("4567".to_string()),
202 r#type: FieldType::Hidden as u8,
203 linked_id: None,
204 },
205 Field {
206 name: Some("Valid From".to_string()),
207 value: Some("2024-01".to_string()),
208 r#type: FieldType::Text as u8,
209 linked_id: None,
210 },
211 ]
212 )
213 }
214}