Files
Rocketmc/apps/labrinth/src/models/v2/notifications.rs
François-Xavier Talbot 4228a193e9 Charge tax on products (#4361)
* Initial Anrok integration

* Query cache, fmt, clippy

* Fmt

* Use payment intent function in edit_subscription

* Attach Anrok client, use payments in index_billing

* Integrate Anrok with refunds

* Bug fixes

* More bugfixes

* Fix resubscriptions

* Medal promotion bugfixes

* Use stripe metadata constants everywhere

* Pre-fill values in products_tax_identifiers

* Cleanup billing route module

* Cleanup

* Email notification for tax charge

* Don't charge tax on users which haven't been notified of tax change

* Fix taxnotification.amount templates

* Update .env.docker-compose

* Update .env.local

* Clippy

* Fmt

* Query cache

* Periodically update tax amount on upcoming charges

* Fix queries

* Skip indexing tax amount on charges if no charges to process

* chore: query cache, clippy, fmt

* Fix a lot of things

* Remove test code

* chore: query cache, clippy, fmt

* Fix money formatting

* Fix conflicts

* Extra documentation, handle tax association properly

* Track loss in tax drift

* chore: query cache, clippy, fmt

* Add subscription.id variable

* chore: query cache, clippy, fmt

* chore: query cache, clippy, fmt
2025-09-25 11:29:29 +00:00

414 lines
13 KiB
Rust

use crate::models::ids::{ThreadMessageId, VersionId};
use crate::models::v3::billing::PriceDuration;
use crate::models::{
ids::{
NotificationId, OrganizationId, ProjectId, ReportId, TeamId, ThreadId,
UserSubscriptionId,
},
notifications::{Notification, NotificationAction, NotificationBody},
projects::ProjectStatus,
};
use ariadne::ids::UserId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct LegacyNotification {
pub id: NotificationId,
pub user_id: UserId,
pub read: bool,
pub created: DateTime<Utc>,
pub body: LegacyNotificationBody,
// DEPRECATED: use body field instead
#[serde(rename = "type")]
pub type_: Option<String>,
pub title: String,
pub text: String,
pub link: String,
pub actions: Vec<LegacyNotificationAction>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct LegacyNotificationAction {
pub title: String,
/// The route to call when this notification action is called. Formatted HTTP Method, route
pub action_route: (String, String),
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum LegacyNotificationBody {
TaxNotification {
subscription_id: UserSubscriptionId,
old_amount: i64,
old_tax_amount: i64,
new_amount: i64,
new_tax_amount: i64,
billing_interval: PriceDuration,
currency: String,
due: DateTime<Utc>,
service: String,
},
ProjectUpdate {
project_id: ProjectId,
version_id: VersionId,
},
TeamInvite {
project_id: ProjectId,
team_id: TeamId,
invited_by: UserId,
role: String,
},
OrganizationInvite {
organization_id: OrganizationId,
invited_by: UserId,
team_id: TeamId,
role: String,
},
StatusChange {
project_id: ProjectId,
old_status: ProjectStatus,
new_status: ProjectStatus,
},
ModeratorMessage {
thread_id: ThreadId,
message_id: ThreadMessageId,
project_id: Option<ProjectId>,
report_id: Option<ReportId>,
},
LegacyMarkdown {
notification_type: Option<String>,
title: String,
text: String,
link: String,
actions: Vec<NotificationAction>,
},
// In `NotificationBody`, this has the `flow` field, however, don't
// include it here, to be 100% certain we don't end up leaking it
// in site notifications.
ResetPassword,
// Idem as ResetPassword
VerifyEmail,
AuthProviderAdded {
provider: String,
},
AuthProviderRemoved {
provider: String,
},
TwoFactorEnabled,
TwoFactorRemoved,
PasswordChanged,
PasswordRemoved,
EmailChanged {
new_email: String,
to_email: String,
},
PaymentFailed {
amount: String,
service: String,
},
PatCreated {
token_name: String,
},
ModerationMessageReceived {
project_id: ProjectId,
},
ReportStatusUpdated {
report_id: ReportId,
},
ReportSubmitted {
report_id: ReportId,
},
ProjectStatusApproved {
project_id: ProjectId,
},
ProjectStatusNeutral {
project_id: ProjectId,
old_status: ProjectStatus,
new_status: ProjectStatus,
},
ProjectTransferred {
project_id: ProjectId,
// Store only the raw identifiers in legacy body
new_owner_user_id: Option<UserId>,
new_owner_organization_id: Option<OrganizationId>,
},
PayoutAvailable {
amount: f64,
date_available: DateTime<Utc>,
},
Unknown,
}
impl LegacyNotification {
pub fn from(notification: Notification) -> Self {
let type_ = match &notification.body {
NotificationBody::ProjectUpdate { .. } => {
Some("project_update".to_string())
}
NotificationBody::TeamInvite { .. } => {
Some("team_invite".to_string())
}
NotificationBody::OrganizationInvite { .. } => {
Some("organization_invite".to_string())
}
NotificationBody::StatusChange { .. } => {
Some("status_change".to_string())
}
NotificationBody::ModeratorMessage { .. } => {
Some("moderator_message".to_string())
}
NotificationBody::PatCreated { .. } => {
Some("pat_created".to_string())
}
NotificationBody::ModerationMessageReceived { .. } => {
Some("moderation_message_received".to_string())
}
NotificationBody::ReportStatusUpdated { .. } => {
Some("report_status_updated".to_string())
}
NotificationBody::ReportSubmitted { .. } => {
Some("report_submitted".to_string())
}
NotificationBody::ProjectStatusApproved { .. } => {
Some("project_status_approved".to_string())
}
NotificationBody::ProjectStatusNeutral { .. } => {
Some("project_status_neutral".to_string())
}
NotificationBody::ProjectTransferred { .. } => {
Some("project_transferred".to_string())
}
NotificationBody::ResetPassword { .. } => {
Some("reset_password".to_string())
}
NotificationBody::VerifyEmail { .. } => {
Some("verify_email".to_string())
}
NotificationBody::AuthProviderAdded { .. } => {
Some("auth_provider_added".to_string())
}
NotificationBody::AuthProviderRemoved { .. } => {
Some("auth_provider_removed".to_string())
}
NotificationBody::TwoFactorEnabled => {
Some("two_factor_enabled".to_string())
}
NotificationBody::TwoFactorRemoved => {
Some("two_factor_removed".to_string())
}
NotificationBody::PasswordChanged => {
Some("password_changed".to_string())
}
NotificationBody::PasswordRemoved => {
Some("password_removed".to_string())
}
NotificationBody::EmailChanged { .. } => {
Some("email_changed".to_string())
}
NotificationBody::PaymentFailed { .. } => {
Some("payment_failed".to_string())
}
NotificationBody::TaxNotification { .. } => {
Some("tax_notification".to_string())
}
NotificationBody::PayoutAvailable { .. } => {
Some("payout_available".to_string())
}
NotificationBody::LegacyMarkdown {
notification_type, ..
} => notification_type.clone(),
NotificationBody::Unknown => None,
};
let legacy_body = match notification.body {
NotificationBody::ProjectUpdate {
project_id,
version_id,
} => LegacyNotificationBody::ProjectUpdate {
project_id,
version_id,
},
NotificationBody::TeamInvite {
project_id,
team_id,
invited_by,
role,
} => LegacyNotificationBody::TeamInvite {
project_id,
team_id,
invited_by,
role,
},
NotificationBody::OrganizationInvite {
organization_id,
invited_by,
team_id,
role,
} => LegacyNotificationBody::OrganizationInvite {
organization_id,
invited_by,
team_id,
role,
},
NotificationBody::StatusChange {
project_id,
old_status,
new_status,
} => LegacyNotificationBody::StatusChange {
project_id,
old_status,
new_status,
},
NotificationBody::ModeratorMessage {
thread_id,
message_id,
project_id,
report_id,
} => LegacyNotificationBody::ModeratorMessage {
thread_id,
message_id,
project_id,
report_id,
},
NotificationBody::PatCreated { token_name } => {
LegacyNotificationBody::PatCreated { token_name }
}
NotificationBody::ModerationMessageReceived { project_id } => {
LegacyNotificationBody::ModerationMessageReceived { project_id }
}
NotificationBody::ReportStatusUpdated { report_id } => {
LegacyNotificationBody::ReportStatusUpdated { report_id }
}
NotificationBody::ReportSubmitted { report_id } => {
LegacyNotificationBody::ReportSubmitted { report_id }
}
NotificationBody::ProjectStatusApproved { project_id } => {
LegacyNotificationBody::ProjectStatusApproved { project_id }
}
NotificationBody::ProjectStatusNeutral {
project_id,
old_status,
new_status,
} => LegacyNotificationBody::ProjectStatusNeutral {
project_id,
old_status,
new_status,
},
NotificationBody::ProjectTransferred {
project_id,
new_owner_user_id,
new_owner_organization_id,
} => LegacyNotificationBody::ProjectTransferred {
project_id,
new_owner_user_id,
new_owner_organization_id,
},
NotificationBody::PayoutAvailable {
amount,
date_available,
} => LegacyNotificationBody::PayoutAvailable {
amount,
date_available,
},
NotificationBody::LegacyMarkdown {
notification_type,
name,
text,
link,
actions,
} => LegacyNotificationBody::LegacyMarkdown {
notification_type,
title: name,
text,
link,
actions,
},
NotificationBody::ResetPassword { .. } => {
LegacyNotificationBody::ResetPassword
}
NotificationBody::VerifyEmail { .. } => {
LegacyNotificationBody::VerifyEmail
}
NotificationBody::AuthProviderAdded { provider } => {
LegacyNotificationBody::AuthProviderAdded { provider }
}
NotificationBody::AuthProviderRemoved { provider } => {
LegacyNotificationBody::AuthProviderRemoved { provider }
}
NotificationBody::TwoFactorEnabled => {
LegacyNotificationBody::TwoFactorEnabled
}
NotificationBody::TwoFactorRemoved => {
LegacyNotificationBody::TwoFactorRemoved
}
NotificationBody::PasswordChanged => {
LegacyNotificationBody::PasswordChanged
}
NotificationBody::PasswordRemoved => {
LegacyNotificationBody::PasswordRemoved
}
NotificationBody::EmailChanged {
new_email,
to_email,
} => LegacyNotificationBody::EmailChanged {
new_email,
to_email,
},
NotificationBody::TaxNotification {
subscription_id,
old_amount,
old_tax_amount,
new_amount,
new_tax_amount,
billing_interval,
currency,
due,
service,
} => LegacyNotificationBody::TaxNotification {
subscription_id,
old_amount,
old_tax_amount,
new_amount,
new_tax_amount,
billing_interval,
due,
service,
currency,
},
NotificationBody::PaymentFailed { amount, service } => {
LegacyNotificationBody::PaymentFailed { amount, service }
}
NotificationBody::Unknown => LegacyNotificationBody::Unknown,
};
Self {
id: notification.id,
user_id: notification.user_id,
read: notification.read,
created: notification.created,
body: legacy_body,
type_,
title: notification.name,
text: notification.text,
link: notification.link,
actions: notification
.actions
.into_iter()
.map(LegacyNotificationAction::from)
.collect(),
}
}
}
impl LegacyNotificationAction {
pub fn from(notification_action: NotificationAction) -> Self {
Self {
title: notification_action.name,
action_route: notification_action.action_route,
}
}
}