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
This commit is contained in:
François-Xavier Talbot
2025-09-25 12:29:29 +01:00
committed by GitHub
parent 47020f34b6
commit 4228a193e9
44 changed files with 3438 additions and 1330 deletions

View File

@@ -1,7 +1,9 @@
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,
@@ -37,6 +39,17 @@ pub struct LegacyNotificationAction {
#[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,
@@ -198,6 +211,9 @@ impl LegacyNotification {
NotificationBody::PaymentFailed { .. } => {
Some("payment_failed".to_string())
}
NotificationBody::TaxNotification { .. } => {
Some("tax_notification".to_string())
}
NotificationBody::PayoutAvailable { .. } => {
Some("payout_available".to_string())
}
@@ -341,6 +357,27 @@ impl LegacyNotification {
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 }
}

View File

@@ -66,6 +66,15 @@ pub enum Price {
},
}
impl Price {
pub fn get_interval(&self, interval: PriceDuration) -> Option<i32> {
match self {
Price::OneTime { .. } => None,
Price::Recurring { intervals } => intervals.get(&interval).copied(),
}
}
}
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq, Debug, Copy, Clone)]
#[serde(rename_all = "kebab-case")]
pub enum PriceDuration {
@@ -175,6 +184,16 @@ pub enum SubscriptionMetadata {
Medal { id: String },
}
impl SubscriptionMetadata {
pub fn is_medal(&self) -> bool {
matches!(self, SubscriptionMetadata::Medal { .. })
}
pub fn is_pyro(&self) -> bool {
matches!(self, SubscriptionMetadata::Pyro { .. })
}
}
#[derive(Serialize, Deserialize)]
pub struct Charge {
pub id: ChargeId,

View File

@@ -2,6 +2,7 @@ use super::ids::*;
use crate::database::models::notification_item::DBNotification;
use crate::database::models::notification_item::DBNotificationAction;
use crate::database::models::notifications_deliveries_item::DBNotificationDelivery;
use crate::models::billing::PriceDuration;
use crate::models::ids::{
NotificationId, ProjectId, ReportId, TeamId, ThreadId, ThreadMessageId,
VersionId,
@@ -46,6 +47,7 @@ pub enum NotificationType {
PasswordRemoved,
EmailChanged,
PaymentFailed,
TaxNotification,
PatCreated,
ModerationMessageReceived,
ReportStatusUpdated,
@@ -76,7 +78,9 @@ impl NotificationType {
NotificationType::PasswordRemoved => "password_removed",
NotificationType::EmailChanged => "email_changed",
NotificationType::PaymentFailed => "payment_failed",
NotificationType::TaxNotification => "tax_notification",
NotificationType::PatCreated => "pat_created",
NotificationType::PayoutAvailable => "payout_available",
NotificationType::ModerationMessageReceived => {
"moderation_message_received"
}
@@ -87,7 +91,6 @@ impl NotificationType {
}
NotificationType::ProjectStatusNeutral => "project_status_neutral",
NotificationType::ProjectTransferred => "project_transferred",
NotificationType::PayoutAvailable => "payout_available",
NotificationType::Unknown => "unknown",
}
}
@@ -110,18 +113,7 @@ impl NotificationType {
"password_removed" => NotificationType::PasswordRemoved,
"email_changed" => NotificationType::EmailChanged,
"payment_failed" => NotificationType::PaymentFailed,
"pat_created" => NotificationType::PatCreated,
"moderation_message_received" => {
NotificationType::ModerationMessageReceived
}
"report_status_updated" => NotificationType::ReportStatusUpdated,
"report_submitted" => NotificationType::ReportSubmitted,
"project_status_approved" => {
NotificationType::ProjectStatusApproved
}
"project_status_neutral" => NotificationType::ProjectStatusNeutral,
"project_transferred" => NotificationType::ProjectTransferred,
"payout_available" => NotificationType::PayoutAvailable,
"tax_notification" => NotificationType::TaxNotification,
"unknown" => NotificationType::Unknown,
_ => NotificationType::Unknown,
}
@@ -218,6 +210,17 @@ pub enum NotificationBody {
amount: String,
service: String,
},
TaxNotification {
subscription_id: UserSubscriptionId,
new_amount: i64,
new_tax_amount: i64,
old_amount: i64,
old_tax_amount: i64,
billing_interval: PriceDuration,
currency: String,
due: DateTime<Utc>,
service: String,
},
PayoutAvailable {
date_available: DateTime<Utc>,
amount: f64,
@@ -293,6 +296,9 @@ impl NotificationBody {
NotificationBody::PaymentFailed { .. } => {
NotificationType::PaymentFailed
}
NotificationBody::TaxNotification { .. } => {
NotificationType::TaxNotification
}
NotificationBody::PayoutAvailable { .. } => {
NotificationType::PayoutAvailable
}
@@ -522,6 +528,12 @@ impl From<DBNotification> for Notification {
"#".to_string(),
vec![],
),
NotificationBody::TaxNotification { .. } => (
"Tax notification".to_string(),
"You've received a tax notification.".to_string(),
"#".to_string(),
vec![],
),
NotificationBody::PayoutAvailable { .. } => (
"Payout available".to_string(),
"A payout is available!".to_string(),