use super::*;
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
pub enum PaymentAttemptFingerprintError {
#[error("payment attempt fingerprint is empty")]
Empty,
}
#[derive(Clone, Eq, Hash, PartialEq)]
pub struct PaymentAttemptFingerprint(String);
impl PaymentAttemptFingerprint {
pub fn new(value: impl Into<String>) -> Result<Self, PaymentAttemptFingerprintError> {
let value = value.into();
if value.trim().is_empty() {
return Err(PaymentAttemptFingerprintError::Empty);
}
Ok(Self(value))
}
pub fn expose(&self) -> &str {
&self.0
}
pub fn for_host_charge(target_id: HostChargeTargetId, amount: Money) -> Self {
Self(format!(
"host_charge:{target_id}:{}:{}",
amount.cents(),
amount.currency().as_str(),
))
}
pub fn for_subscription_initial_v1(
plan_key: &PlanKey,
amount: Money,
discount: Option<&SubscriptionEnrollmentDiscountSnapshot>,
) -> Self {
let currency_code = amount.currency();
let currency = currency_code.as_str();
let authoritative = match discount {
Some(discount) => {
let snapshot = discount.snapshot();
format!(
"subscription_initial:{plan_key}:{}:{currency}:discount:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}",
amount.cents(),
discount.claim_id(),
discount.code_id(),
snapshot.code().as_str(),
snapshot.kind().as_str(),
discount_amount_off(snapshot.kind()),
discount_percent_off(snapshot.kind()),
snapshot.currency().as_str(),
snapshot.base_charge().cents(),
snapshot.discounted_charge().cents(),
snapshot.duration().as_str(),
discount_duration_months(snapshot.duration()),
)
}
None => format!(
"subscription_initial:{plan_key}:{}:{currency}:discount:none",
amount.cents()
),
};
let expected =
subscription_initial_expected_fingerprint(amount, discount.map(|d| d.snapshot()));
Self(format!("{authoritative}:expected:{expected}"))
}
pub fn matches_subscription_initial_expected_terms_v1(
&self,
plan_key: &PlanKey,
expected: &crate::SubscriptionEnrollmentExpectedTerms,
) -> bool {
if expected.plan_key() != plan_key {
return false;
}
let expected_fingerprint = subscription_initial_expected_fingerprint(
expected.initial_charge().money(),
expected.discount_snapshot(),
);
self.0
.ends_with(&format!(":expected:{expected_fingerprint}"))
}
pub fn for_subscription_initial_v2(
offer: &SubscriptionOffer,
amount: Money,
discount: Option<&SubscriptionEnrollmentDiscountSnapshot>,
) -> Self {
let start = match offer.start() {
crate::SubscriptionStart::RecurringImmediately => {
"recurring_immediately:trial:none".to_owned()
}
crate::SubscriptionStart::PaidTrial(trial) => format!(
"paid_trial:trial:{}:{}:{}:{}",
trial.charge().cents(),
trial.charge().currency().as_str(),
trial.period().as_str(),
trial.period().count(),
),
};
let recurring = offer.recurring();
let retry_delays = offer
.renewal_failure()
.schedule()
.retry_delays()
.iter()
.map(|delay| delay.seconds().get().to_string())
.collect::<Vec<_>>()
.join(",");
let discount = match discount {
Some(discount) => {
let snapshot = discount.snapshot();
format!(
"{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}",
discount.claim_id(),
discount.code_id(),
snapshot.code().as_str(),
snapshot.kind().as_str(),
discount_amount_off(snapshot.kind()),
discount_percent_off(snapshot.kind()),
snapshot.currency().as_str(),
snapshot.base_charge().cents(),
snapshot.discounted_charge().cents(),
snapshot.duration().as_str(),
discount_duration_months(snapshot.duration()),
)
}
None => "none".to_owned(),
};
Self(format!(
"subscription_initial:v2:{}:start:{start}:recurring:{}:{}:{}:{}:dunning:[{retry_delays}]:{}:{}:initial:{}:{}:discount:{discount}",
offer.plan_key(),
recurring.charge().cents(),
recurring.charge().currency().as_str(),
recurring.period().as_str(),
recurring.period().count(),
offer.renewal_failure().exhaustion().as_str(),
offer.renewal_failure().past_due_access().as_str(),
amount.cents(),
amount.currency().as_str(),
))
}
pub fn matches_subscription_initial_v2(
&self,
offer: &SubscriptionOffer,
amount: Money,
discount: Option<&SubscriptionEnrollmentDiscountSnapshot>,
) -> bool {
self == &Self::for_subscription_initial_v2(offer, amount, discount)
}
pub fn for_subscription_recovery(
plan_key: &PlanKey,
subscription_id: SubscriptionId,
payment_method_id: PaymentMethodId,
period_start_at: DateTime<Utc>,
amount: Money,
) -> Self {
Self(format!(
"subscription_recovery:{plan_key}:{subscription_id}:{payment_method_id}:{period_start_at}:{}:{}",
amount.cents(),
amount.currency().as_str(),
))
}
pub fn matches_subscription_recovery(
&self,
plan_key: &PlanKey,
subscription_id: SubscriptionId,
payment_method_id: PaymentMethodId,
period_start_at: DateTime<Utc>,
amount: Money,
) -> bool {
self == &Self::for_subscription_recovery(
plan_key,
subscription_id,
payment_method_id,
period_start_at,
amount,
)
}
pub fn for_subscription_renewal(
plan_key: &PlanKey,
subscription_id: SubscriptionId,
payment_method_id: PaymentMethodId,
period_start_at: DateTime<Utc>,
amount: Money,
) -> Self {
Self(format!(
"subscription_renewal:{plan_key}:{subscription_id}:{payment_method_id}:{period_start_at}:{}:{}",
amount.cents(),
amount.currency().as_str(),
))
}
pub fn matches_subscription_renewal(
&self,
plan_key: &PlanKey,
subscription_id: SubscriptionId,
payment_method_id: PaymentMethodId,
period_start_at: DateTime<Utc>,
amount: Money,
) -> bool {
self == &Self::for_subscription_renewal(
plan_key,
subscription_id,
payment_method_id,
period_start_at,
amount,
)
}
pub fn for_subscription_payment_method_update(
plan_key: &PlanKey,
subscription_id: SubscriptionId,
payment_method_id: PaymentMethodId,
expected_initial_transaction_id: &GatewayTransactionId,
) -> Self {
Self(format!(
"subscription_payment_method_update:{plan_key}:{subscription_id}:{payment_method_id}:{}",
expected_initial_transaction_id.expose(),
))
}
pub fn matches_subscription_payment_method_update(
&self,
plan_key: &PlanKey,
expected: &PaymentMethodUpdateSnapshot,
) -> bool {
self == &Self::for_subscription_payment_method_update(
plan_key,
expected.subscription_id(),
expected.payment_method_id(),
expected.expected_initial_transaction_id(),
)
}
}
fn subscription_initial_expected_fingerprint(
amount: Money,
discount: Option<&crate::SubscriptionDiscountSnapshot>,
) -> String {
let currency_code = amount.currency();
let currency = currency_code.as_str();
match discount {
Some(snapshot) => format!(
"discounted:{}:{}:{}:{}:{}:{}:{}:{}:{}",
snapshot.code().as_str(),
snapshot.kind().as_str(),
discount_amount_off(snapshot.kind()),
discount_percent_off(snapshot.kind()),
snapshot.duration().as_str(),
discount_duration_months(snapshot.duration()),
snapshot.currency().as_str(),
snapshot.base_charge().cents(),
snapshot.discounted_charge().cents(),
),
None => format!("full_price:{}:{currency}", amount.cents()),
}
}
fn discount_amount_off(kind: SubscriptionDiscountKind) -> String {
match kind {
SubscriptionDiscountKind::AmountOffCents(value) => value.get().to_string(),
SubscriptionDiscountKind::PercentOffBasisPoints(_) => "none".to_owned(),
}
}
fn discount_percent_off(kind: SubscriptionDiscountKind) -> String {
match kind {
SubscriptionDiscountKind::AmountOffCents(_) => "none".to_owned(),
SubscriptionDiscountKind::PercentOffBasisPoints(value) => value.get().to_string(),
}
}
fn discount_duration_months(duration: SubscriptionDiscountDuration) -> String {
match duration {
SubscriptionDiscountDuration::Indefinite => "none".to_owned(),
SubscriptionDiscountDuration::LimitedMonths(value) => value.get().to_string(),
}
}
impl fmt::Debug for PaymentAttemptFingerprint {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("PaymentAttemptFingerprint([redacted])")
}
}
impl fmt::Display for PaymentAttemptFingerprint {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("[redacted]")
}
}