Bug: [FEATURE] Validate card security codes and expiration month/year



### Feature Description

As of now, we don't perform any validations on card security codes (CVC/CVV/CID code) and card expiration month and year. It'd be great if the following validations can be done:

1. Card security code must be 3 or 4 digits only
2. Expiration month is valid (1-12)
3. Expiration year is a 4 digit number only
4. Card has not expired (expiration month and year correspond to a date in the future)

This logic can reside in a new crate called `cards`. Please also include unit tests in your PR.

_For external contributors, clarify any questions you may have, and let us know that you're interested in picking this up. We'll assign this issue to you to reduce duplication of effort on the same task._

### Possible Implementation

For card security codes, you could do:

```rust
use masking::StrongSecret;

struct CardSecurityCode(StrongSecret<u16>);

let card_security_code = CardSecurityCode::try_from(123); // You can use the `TryFrom` trait for this purpose

struct CardExpirationMonth(StrongSecret<u8>);

let card_expiration_month = CardExpirationMonth::try_from(1); // You can use the `TryFrom` trait for this purpose

impl CardExpirationMonth {
    fn inner(&self) -> u8;
    fn two_digits(&self) -> String;
}

struct CardExpirationYear(StrongSecret<u16>);

let card_expiration_year = CardExpirationYear::try_from(2038); // You can use the `TryFrom` trait for this purpose

impl CardExpirationYear {
    fn inner(&self) -> u16;
    fn four_digits(&self) -> String;
    fn two_digits(&self) -> String;
}

struct CardExpiration {
    pub month: CardExpirationMonth,
    pub year: CardExpirationYear,
}

impl CardExpiration {
    fn is_expired(&self) -> bool;
}
```

You can implement `Serialize` and `Deserialize` traits on `CardSecurityCode`. Use the 2-digit and 4-digit representations for the month and year, respectively, for the `Serialize` and `Deserialize` implementations of the card expiry month and year. You can implement any more methods as necessary.

### Have you spent some time to check if this feature request has been raised before?

- [X] I checked and didn't find similar issue

### Have you read the Contributing Guidelines?

- [X] I have read the [Contributing Guidelines](https://github.com/juspay/hyperswitch/blob/main/docs/CONTRIBUTING.md)

### Are you willing to submit a PR?

None
diff --git a/Cargo.lock b/Cargo.lock
index d065e56825f..3e9de17d58d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1148,6 +1148,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "cards"
+version = "0.1.0"
+dependencies = [
+ "common_utils",
+ "error-stack",
+ "masking",
+ "serde",
+ "serde_json",
+ "time",
+]
+
 [[package]]
 name = "cargo-platform"
 version = "0.1.2"
@@ -2937,7 +2949,7 @@ dependencies = [
 [[package]]
 name = "opentelemetry"
 version = "0.18.0"
-source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
+source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
 dependencies = [
  "opentelemetry_api",
  "opentelemetry_sdk",
@@ -2946,7 +2958,7 @@ dependencies = [
 [[package]]
 name = "opentelemetry-otlp"
 version = "0.11.0"
-source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
+source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
 dependencies = [
  "async-trait",
  "futures",
@@ -2963,7 +2975,7 @@ dependencies = [
 [[package]]
 name = "opentelemetry-proto"
 version = "0.1.0"
-source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
+source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
 dependencies = [
  "futures",
  "futures-util",
@@ -2975,7 +2987,7 @@ dependencies = [
 [[package]]
 name = "opentelemetry_api"
 version = "0.18.0"
-source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
+source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
 dependencies = [
  "fnv",
  "futures-channel",
@@ -2990,7 +3002,7 @@ dependencies = [
 [[package]]
 name = "opentelemetry_sdk"
 version = "0.18.0"
-source = "git+https://github.com/open-telemetry/opentelemetry-rust/?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
+source = "git+https://github.com/open-telemetry/opentelemetry-rust?rev=44b90202fd744598db8b0ace5b8f0bad7ec45658#44b90202fd744598db8b0ace5b8f0bad7ec45658"
 dependencies = [
  "async-trait",
  "crossbeam-channel",
diff --git a/crates/cards/Cargo.toml b/crates/cards/Cargo.toml
new file mode 100644
index 00000000000..7971726c9a6
--- /dev/null
+++ b/crates/cards/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "cards"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[features]
+default = ["serde"]
+
+[dependencies]
+time = { version = "0.3.20" }
+error-stack = "0.3.1"
+serde = { version = "1", features = ["derive"], optional = true }
+
+# First Party crates
+masking = { version = "0.1.0", path = "../masking" }
+common_utils = { version = "0.1.0", path = "../common_utils" }
+
+[dev-dependencies]
+serde_json = "1.0.94"
\ No newline at end of file
diff --git a/crates/cards/src/lib.rs b/crates/cards/src/lib.rs
new file mode 100644
index 00000000000..00bd0578c3d
--- /dev/null
+++ b/crates/cards/src/lib.rs
@@ -0,0 +1,190 @@
+use std::ops::Deref;
+
+use common_utils::{date_time, errors};
+use error_stack::report;
+use masking::{PeekInterface, StrongSecret};
+use serde::{
+    de::{self},
+    Deserialize, Serialize,
+};
+use time::{util::days_in_year_month, Date, Duration, PrimitiveDateTime, Time};
+
+#[derive(Serialize)]
+pub struct CardSecurityCode(StrongSecret<u16>);
+
+impl TryFrom<u16> for CardSecurityCode {
+    type Error = error_stack::Report<errors::ValidationError>;
+    fn try_from(csc: u16) -> Result<Self, Self::Error> {
+        if (100..=9999).contains(&csc) {
+            Ok(Self(StrongSecret::new(csc)))
+        } else {
+            Err(report!(errors::ValidationError::InvalidValue {
+                message: "invalid card security code".to_string()
+            }))
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for CardSecurityCode {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let csc = u16::deserialize(deserializer)?;
+        csc.try_into().map_err(de::Error::custom)
+    }
+}
+
+#[derive(Serialize)]
+pub struct CardExpirationMonth(StrongSecret<u8>);
+
+impl CardExpirationMonth {
+    pub fn two_digits(&self) -> String {
+        format!("{:02}", self.peek())
+    }
+}
+
+impl TryFrom<u8> for CardExpirationMonth {
+    type Error = error_stack::Report<errors::ValidationError>;
+    fn try_from(month: u8) -> Result<Self, Self::Error> {
+        if (1..=12).contains(&month) {
+            Ok(Self(StrongSecret::new(month)))
+        } else {
+            Err(report!(errors::ValidationError::InvalidValue {
+                message: "invalid card expiration month".to_string()
+            }))
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for CardExpirationMonth {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let month = u8::deserialize(deserializer)?;
+        month.try_into().map_err(de::Error::custom)
+    }
+}
+
+#[derive(Serialize)]
+pub struct CardExpirationYear(StrongSecret<u16>);
+
+impl CardExpirationYear {
+    pub fn four_digits(&self) -> String {
+        self.peek().to_string()
+    }
+
+    pub fn two_digits(&self) -> String {
+        let year = self.peek() % 100;
+        year.to_string()
+    }
+}
+
+impl TryFrom<u16> for CardExpirationYear {
+    type Error = error_stack::Report<errors::ValidationError>;
+    fn try_from(year: u16) -> Result<Self, Self::Error> {
+        let curr_year = u16::try_from(date_time::now().year()).map_err(|_| {
+            report!(errors::ValidationError::InvalidValue {
+                message: "invalid year".to_string()
+            })
+        })?;
+
+        if year >= curr_year {
+            Ok(Self(StrongSecret::<u16>::new(year)))
+        } else {
+            Err(report!(errors::ValidationError::InvalidValue {
+                message: "invalid card expiration year".to_string()
+            }))
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for CardExpirationYear {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let year = u16::deserialize(deserializer)?;
+        year.try_into().map_err(de::Error::custom)
+    }
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct CardExpiration {
+    pub month: CardExpirationMonth,
+    pub year: CardExpirationYear,
+}
+
+impl CardExpiration {
+    pub fn is_expired(&self) -> Result<bool, error_stack::Report<errors::ValidationError>> {
+        let current_datetime_utc = date_time::now();
+
+        let expiration_month = (*self.month.peek()).try_into().map_err(|_| {
+            report!(errors::ValidationError::InvalidValue {
+                message: "invalid month".to_string()
+            })
+        })?;
+
+        let expiration_year = *self.year.peek();
+
+        let expiration_day = days_in_year_month(i32::from(expiration_year), expiration_month);
+
+        let expiration_date =
+            Date::from_calendar_date(i32::from(expiration_year), expiration_month, expiration_day)
+                .map_err(|_| {
+                    report!(errors::ValidationError::InvalidValue {
+                        message: "error while constructing calendar date".to_string()
+                    })
+                })?;
+
+        let expiration_time = Time::MIDNIGHT;
+
+        // actual expiry date specified on card w.r.t. local timezone
+        // max diff b/w utc and other timezones is 14 hours
+        let mut expiration_datetime_utc = PrimitiveDateTime::new(expiration_date, expiration_time);
+
+        // compensating time difference b/w local and utc timezone by adding a day
+        expiration_datetime_utc = expiration_datetime_utc.saturating_add(Duration::days(1));
+
+        Ok(current_datetime_utc > expiration_datetime_utc)
+    }
+
+    pub fn get_month(&self) -> &CardExpirationMonth {
+        &self.month
+    }
+
+    pub fn get_year(&self) -> &CardExpirationYear {
+        &self.year
+    }
+}
+
+impl TryFrom<(u8, u16)> for CardExpiration {
+    type Error = error_stack::Report<errors::ValidationError>;
+    fn try_from(items: (u8, u16)) -> errors::CustomResult<Self, errors::ValidationError> {
+        let month = CardExpirationMonth::try_from(items.0)?;
+        let year = CardExpirationYear::try_from(items.1)?;
+        Ok(Self { month, year })
+    }
+}
+
+impl Deref for CardSecurityCode {
+    type Target = StrongSecret<u16>;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl Deref for CardExpirationMonth {
+    type Target = StrongSecret<u8>;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl Deref for CardExpirationYear {
+    type Target = StrongSecret<u16>;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
diff --git a/crates/cards/tests/basic.rs b/crates/cards/tests/basic.rs
new file mode 100644
index 00000000000..a2e01d64063
--- /dev/null
+++ b/crates/cards/tests/basic.rs
@@ -0,0 +1,101 @@
+#![allow(clippy::unwrap_used, clippy::expect_used)]
+
+use cards::{CardExpiration, CardExpirationMonth, CardExpirationYear, CardSecurityCode};
+use common_utils::date_time;
+use masking::PeekInterface;
+
+#[test]
+fn test_card_security_code() {
+    // no panic
+    let valid_card_security_code = CardSecurityCode::try_from(1234).unwrap();
+
+    // will panic on unwrap
+    let invalid_card_security_code = CardSecurityCode::try_from(12);
+
+    assert_eq!(*valid_card_security_code.peek(), 1234);
+    assert!(invalid_card_security_code.is_err());
+
+    let serialized = serde_json::to_string(&valid_card_security_code).unwrap();
+    assert_eq!(serialized, "1234");
+
+    let derialized = serde_json::from_str::<CardSecurityCode>(&serialized).unwrap();
+    assert_eq!(*derialized.peek(), 1234);
+
+    let invalid_deserialization = serde_json::from_str::<CardSecurityCode>("12");
+    assert!(invalid_deserialization.is_err());
+}
+
+#[test]
+fn test_card_expiration_month() {
+    // no panic
+    let card_exp_month = CardExpirationMonth::try_from(12).unwrap();
+
+    // will panic on unwrap
+    let invalid_card_exp_month = CardExpirationMonth::try_from(13);
+
+    assert_eq!(*card_exp_month.peek(), 12);
+    assert!(invalid_card_exp_month.is_err());
+
+    let serialized = serde_json::to_string(&card_exp_month).unwrap();
+    assert_eq!(serialized, "12");
+
+    let derialized = serde_json::from_str::<CardExpirationMonth>(&serialized).unwrap();
+    assert_eq!(*derialized.peek(), 12);
+
+    let invalid_deserialization = serde_json::from_str::<CardExpirationMonth>("13");
+    assert!(invalid_deserialization.is_err());
+}
+
+#[test]
+fn test_card_expiration_year() {
+    let curr_date = date_time::now();
+    let curr_year = u16::try_from(curr_date.year()).expect("valid year");
+
+    // no panic
+    let card_exp_year = CardExpirationYear::try_from(curr_year).unwrap();
+
+    // will panic on unwrap
+    let invalid_card_exp_year = CardExpirationYear::try_from(curr_year - 1);
+
+    assert_eq!(*card_exp_year.peek(), curr_year);
+    assert!(invalid_card_exp_year.is_err());
+
+    let serialized = serde_json::to_string(&card_exp_year).unwrap();
+    assert_eq!(serialized, curr_year.to_string());
+
+    let derialized = serde_json::from_str::<CardExpirationYear>(&serialized).unwrap();
+    assert_eq!(*derialized.peek(), curr_year);
+
+    let invalid_deserialization = serde_json::from_str::<CardExpirationYear>("123");
+    assert!(invalid_deserialization.is_err());
+}
+
+#[test]
+fn test_card_expiration() {
+    let curr_date = date_time::now();
+    let curr_year = u16::try_from(curr_date.year()).expect("valid year");
+
+    // no panic
+    let card_exp = CardExpiration::try_from((3, curr_year + 1)).unwrap();
+
+    // will panic on unwrap
+    let invalid_card_exp = CardExpiration::try_from((13, curr_year + 1));
+
+    assert_eq!(*card_exp.get_month().peek(), 3);
+    assert_eq!(*card_exp.get_year().peek(), curr_year + 1);
+    assert!(card_exp.is_expired().unwrap());
+
+    assert!(invalid_card_exp.is_err());
+
+    let serialized = serde_json::to_string(&card_exp).unwrap();
+    let expected_string = format!(r#"{{"month":{},"year":{}}}"#, 3, curr_year + 1);
+    assert_eq!(serialized, expected_string);
+
+    let derialized = serde_json::from_str::<CardExpiration>(&serialized).unwrap();
+    assert_eq!(*derialized.get_month().peek(), 3);
+    assert_eq!(*derialized.get_year().peek(), curr_year + 1);
+
+    let invalid_serialized_string = r#"{"month":13,"year":123}"#;
+    let invalid_deserialization = serde_json::from_str::<CardExpiration>(invalid_serialized_string);
+    assert!(invalid_deserialization.is_err());
+}
diff --git a/crates/masking/src/serde.rs b/crates/masking/src/serde.rs
index b0f300f0879..abdc117b612 100644
--- a/crates/masking/src/serde.rs
+++ b/crates/masking/src/serde.rs
@@ -23,6 +23,8 @@ pub trait SerializableSecret: Serialize {}
 // pub trait NonSerializableSecret: Serialize {}
 
 impl SerializableSecret for serde_json::Value {}
+impl SerializableSecret for u8 {}
+impl SerializableSecret for u16 {}
 
 impl<'de, T, I> Deserialize<'de> for Secret<T, I>
 where

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates

## Description
Add basic validations for card security codes (CVC/CVV/CID code) and card expiration month and year. This logic resides in a new crate called cards. Validations included (as of now):
 - [x] Card security code must be 3 or 4 digits only
 - [x] Expiration month is valid (1-12)
 - [x] Expiration year is a 4 digit number only
 - [x] Card has not expired (expiration month and year correspond to a date in the future)


### Additional Changes

- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!-- 
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless its an obvious bug or documentation fix
that will have little conversation).
-->
This fixes #607 

## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
run `cargo test --cards` 

## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed submitted code
- [x] I added unit tests for my changes where possible
- [ ] I added a [CHANGELOG](/CHANGELOG.md) entry if applicable
