From d7d740195cb4067f23fd8b5623018740f517db18 Mon Sep 17 00:00:00 2001 From: fetch Date: Thu, 25 Sep 2025 15:42:58 +0200 Subject: [PATCH 1/6] Only update the PaymentMethod ID if not using placeholder ID --- .../labrinth/src/routes/internal/billing/payments.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/labrinth/src/routes/internal/billing/payments.rs b/apps/labrinth/src/routes/internal/billing/payments.rs index ca1b9503b1..f789ccd181 100644 --- a/apps/labrinth/src/routes/internal/billing/payments.rs +++ b/apps/labrinth/src/routes/internal/billing/payments.rs @@ -530,15 +530,23 @@ pub async fn create_or_update_payment_intent( } if let Some(payment_intent_id) = existing_payment_intent { - let update_payment_intent = stripe::UpdatePaymentIntent { + let mut update_payment_intent = stripe::UpdatePaymentIntent { amount: Some(charge_data.amount + tax_amount), currency: Some(inferred_stripe_currency), customer: Some(customer_id), metadata: Some(metadata), - payment_method: Some(payment_method.id.clone()), ..Default::default() }; + // If the payment request type was done through a confirmation token, + if let PaymentSession::Interactive { + payment_request_type: PaymentRequestType::PaymentMethod { .. }, + } = &payment_session + { + update_payment_intent.payment_method = + Some(payment_method.id.clone()); + } + stripe::PaymentIntent::update( stripe_client, &payment_intent_id, From e8f833ecdf96e38969c6ebd0a2523367854941c0 Mon Sep 17 00:00:00 2001 From: fetch Date: Thu, 25 Sep 2025 15:43:50 +0200 Subject: [PATCH 2/6] comment --- apps/labrinth/src/routes/internal/billing/payments.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/labrinth/src/routes/internal/billing/payments.rs b/apps/labrinth/src/routes/internal/billing/payments.rs index f789ccd181..ec5819a8f6 100644 --- a/apps/labrinth/src/routes/internal/billing/payments.rs +++ b/apps/labrinth/src/routes/internal/billing/payments.rs @@ -539,6 +539,11 @@ pub async fn create_or_update_payment_intent( }; // If the payment request type was done through a confirmation token, + // the payment method ID is an invalid placeholder so we don't want + // to use it. + // + // The PaymentIntent will be confirmed using the confirmation token + // by the client. if let PaymentSession::Interactive { payment_request_type: PaymentRequestType::PaymentMethod { .. }, } = &payment_session From 4334c3b58940d71f2dc8b9c591a3b1b3a22ca527 Mon Sep 17 00:00:00 2001 From: fetch Date: Thu, 25 Sep 2025 16:00:14 +0200 Subject: [PATCH 3/6] Create Anrok transactions for all charges --- apps/labrinth/src/database/models/charge_item.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/labrinth/src/database/models/charge_item.rs b/apps/labrinth/src/database/models/charge_item.rs index 272bf004e8..4a83b4a24c 100644 --- a/apps/labrinth/src/database/models/charge_item.rs +++ b/apps/labrinth/src/database/models/charge_item.rs @@ -349,7 +349,6 @@ impl DBCharge { WHERE status = 'succeeded' AND tax_platform_id IS NULL - AND tax_amount <> 0 ORDER BY due ASC FOR NO KEY UPDATE SKIP LOCKED LIMIT $1 From 55cced1639277a20fb0f25a947c0dbcd73d728b5 Mon Sep 17 00:00:00 2001 From: fetch Date: Thu, 25 Sep 2025 16:08:14 +0200 Subject: [PATCH 4/6] Fix comment --- apps/labrinth/src/database/models/charge_item.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/labrinth/src/database/models/charge_item.rs b/apps/labrinth/src/database/models/charge_item.rs index 4a83b4a24c..1ade9d375d 100644 --- a/apps/labrinth/src/database/models/charge_item.rs +++ b/apps/labrinth/src/database/models/charge_item.rs @@ -336,8 +336,7 @@ impl DBCharge { .collect::, serde_json::Error>>()?) } - /// Returns all charges which are missing a tax identifier, that is, are 1. succeeded, 2. have a tax amount and - /// 3. haven't been assigned a tax identifier yet. + /// Returns all charges which are missing a tax identifier, that is, are succeeded and haven't been assigned a tax identifier yet. /// /// Charges are locked. pub async fn get_missing_tax_identifier_lock( From 217501544c05f426d3138044ec69364929c6c154 Mon Sep 17 00:00:00 2001 From: fetch Date: Thu, 25 Sep 2025 16:16:08 +0200 Subject: [PATCH 5/6] Prefer using payment method's address rather than customer address --- apps/labrinth/src/queue/billing.rs | 108 +++++++++++++++++++---------- 1 file changed, 73 insertions(+), 35 deletions(-) diff --git a/apps/labrinth/src/queue/billing.rs b/apps/labrinth/src/queue/billing.rs index d1e943181e..9e62871fdd 100644 --- a/apps/labrinth/src/queue/billing.rs +++ b/apps/labrinth/src/queue/billing.rs @@ -129,23 +129,6 @@ pub async fn index_subscriptions( let redis = redis_ref.clone(); async move { - let stripe_customer_id = - DBUser::get_id(charge.user_id, &pg, &redis) - .await? - .ok_or_else(|| { - ApiError::from(DatabaseError::Database( - sqlx::Error::RowNotFound, - )) - }) - .and_then(|user| { - user.stripe_customer_id.ok_or_else(|| { - ApiError::InvalidInput( - "User has no Stripe customer ID" - .to_owned(), - ) - }) - })?; - let tax_id = DBProductsTaxIdentifier::get_price( charge.price_id, &pg, @@ -164,26 +147,81 @@ pub async fn index_subscriptions( ) })?; - let Ok(customer_id): Result = - stripe_customer_id.parse() - else { - return Err(ApiError::InvalidInput( - "Charge's Stripe customer ID was invalid" - .to_owned(), - )); - }; + let stripe_address = 'a: { + let stripe_id: stripe::PaymentIntentId = charge + .payment_platform_id + .as_ref() + .and_then(|x| x.parse().ok()) + .ok_or_else(|| { + ApiError::InvalidInput( + "Charge has no payment platform ID" + .to_owned(), + ) + })?; - let customer = stripe::Customer::retrieve( - &stripe_client, - &customer_id, - &[], - ) - .await?; + // Attempt retrieving the address via the payment intent's payment method + + let pi = stripe::PaymentIntent::retrieve( + &stripe_client, + &stripe_id, + &["payment_method"], + ) + .await?; + + let pi_stripe_address = pi + .payment_method + .and_then(|x| x.into_object()) + .and_then(|x| x.billing_details.address); + + match pi_stripe_address { + Some(address) => break 'a address, + None => { + warn!("PaymentMethod had no address"); + } + }; + + let stripe_customer_id = + DBUser::get_id(charge.user_id, &pg, &redis) + .await? + .ok_or_else(|| { + ApiError::from(DatabaseError::Database( + sqlx::Error::RowNotFound, + )) + }) + .and_then(|user| { + user.stripe_customer_id.ok_or_else( + || { + ApiError::InvalidInput( + "User has no Stripe customer ID" + .to_owned(), + ) + }, + ) + })?; + + let Ok(customer_id): Result = + stripe_customer_id.parse() + else { + return Err(ApiError::InvalidInput( + "Charge's Stripe customer ID was invalid" + .to_owned(), + )); + }; + + let customer = stripe::Customer::retrieve( + &stripe_client, + &customer_id, + &[], + ) + .await?; + + let Some(stripe_address) = customer.address else { + return Err(ApiError::InvalidInput( + "Stripe customer had no address".to_owned(), + )); + }; - let Some(stripe_address) = customer.address else { - return Err(ApiError::InvalidInput( - "Stripe customer had no address".to_owned(), - )); + stripe_address }; let customer_address = From ed44d3f5da08e6eb0b7ea2ea9c42cacb2914d8ae Mon Sep 17 00:00:00 2001 From: fetch Date: Thu, 25 Sep 2025 16:24:01 +0200 Subject: [PATCH 6/6] chore: query cache, clippy, fmt --- ...427d5f8a0e4c4e89eb8127f1cb3cd07f89fc2d206e3b5c8805f6.json} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename apps/labrinth/.sqlx/{query-a20149894f03ff65c35f4ce9c2c6e8735867376783d8665036cfc85df3f4867d.json => query-8b4979c9f0eb427d5f8a0e4c4e89eb8127f1cb3cd07f89fc2d206e3b5c8805f6.json} (93%) diff --git a/apps/labrinth/.sqlx/query-a20149894f03ff65c35f4ce9c2c6e8735867376783d8665036cfc85df3f4867d.json b/apps/labrinth/.sqlx/query-8b4979c9f0eb427d5f8a0e4c4e89eb8127f1cb3cd07f89fc2d206e3b5c8805f6.json similarity index 93% rename from apps/labrinth/.sqlx/query-a20149894f03ff65c35f4ce9c2c6e8735867376783d8665036cfc85df3f4867d.json rename to apps/labrinth/.sqlx/query-8b4979c9f0eb427d5f8a0e4c4e89eb8127f1cb3cd07f89fc2d206e3b5c8805f6.json index e49fdf387b..d79efd64f4 100644 --- a/apps/labrinth/.sqlx/query-a20149894f03ff65c35f4ce9c2c6e8735867376783d8665036cfc85df3f4867d.json +++ b/apps/labrinth/.sqlx/query-8b4979c9f0eb427d5f8a0e4c4e89eb8127f1cb3cd07f89fc2d206e3b5c8805f6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n\t\t\tWHERE\n\t\t\t status = 'succeeded'\n\t\t\t AND tax_platform_id IS NULL\n\t\t\t AND tax_amount <> 0\n\t\t\tORDER BY due ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n\t\t\tLIMIT $1\n\t\t\t", + "query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n\t\t\tWHERE\n\t\t\t status = 'succeeded'\n\t\t\t AND tax_platform_id IS NULL\n\t\t\tORDER BY due ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n\t\t\tLIMIT $1\n\t\t\t", "describe": { "columns": [ { @@ -126,5 +126,5 @@ true ] }, - "hash": "a20149894f03ff65c35f4ce9c2c6e8735867376783d8665036cfc85df3f4867d" + "hash": "8b4979c9f0eb427d5f8a0e4c4e89eb8127f1cb3cd07f89fc2d206e3b5c8805f6" }