Billing fixes (#4422)

* Only update the PaymentMethod ID if not using placeholder ID

* comment

* Create Anrok transactions for all charges

* Fix comment

* Prefer using payment method's address rather than customer address

* chore: query cache, clippy, fmt
This commit is contained in:
François-Xavier Talbot
2025-09-26 16:39:47 +01:00
committed by GitHub
parent d43451e398
commit 14af3d0763
4 changed files with 91 additions and 42 deletions

View File

@@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "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": { "describe": {
"columns": [ "columns": [
{ {
@@ -126,5 +126,5 @@
true true
] ]
}, },
"hash": "a20149894f03ff65c35f4ce9c2c6e8735867376783d8665036cfc85df3f4867d" "hash": "8b4979c9f0eb427d5f8a0e4c4e89eb8127f1cb3cd07f89fc2d206e3b5c8805f6"
} }

View File

@@ -336,8 +336,7 @@ impl DBCharge {
.collect::<Result<Vec<_>, serde_json::Error>>()?) .collect::<Result<Vec<_>, serde_json::Error>>()?)
} }
/// Returns all charges which are missing a tax identifier, that is, are 1. succeeded, 2. have a tax amount and /// Returns all charges which are missing a tax identifier, that is, are succeeded and haven't been assigned a tax identifier yet.
/// 3. haven't been assigned a tax identifier yet.
/// ///
/// Charges are locked. /// Charges are locked.
pub async fn get_missing_tax_identifier_lock( pub async fn get_missing_tax_identifier_lock(
@@ -349,7 +348,6 @@ impl DBCharge {
WHERE WHERE
status = 'succeeded' status = 'succeeded'
AND tax_platform_id IS NULL AND tax_platform_id IS NULL
AND tax_amount <> 0
ORDER BY due ASC ORDER BY due ASC
FOR NO KEY UPDATE SKIP LOCKED FOR NO KEY UPDATE SKIP LOCKED
LIMIT $1 LIMIT $1

View File

@@ -129,23 +129,6 @@ pub async fn index_subscriptions(
let redis = redis_ref.clone(); let redis = redis_ref.clone();
async move { 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( let tax_id = DBProductsTaxIdentifier::get_price(
charge.price_id, charge.price_id,
&pg, &pg,
@@ -164,26 +147,81 @@ pub async fn index_subscriptions(
) )
})?; })?;
let Ok(customer_id): Result<stripe::CustomerId, _> = let stripe_address = 'a: {
stripe_customer_id.parse() let stripe_id: stripe::PaymentIntentId = charge
else { .payment_platform_id
return Err(ApiError::InvalidInput( .as_ref()
"Charge's Stripe customer ID was invalid" .and_then(|x| x.parse().ok())
.to_owned(), .ok_or_else(|| {
)); ApiError::InvalidInput(
}; "Charge has no payment platform ID"
.to_owned(),
)
})?;
let customer = stripe::Customer::retrieve( // Attempt retrieving the address via the payment intent's payment method
&stripe_client,
&customer_id,
&[],
)
.await?;
let Some(stripe_address) = customer.address else { let pi = stripe::PaymentIntent::retrieve(
return Err(ApiError::InvalidInput( &stripe_client,
"Stripe customer had no address".to_owned(), &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::CustomerId, _> =
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(),
));
};
stripe_address
}; };
let customer_address = let customer_address =

View File

@@ -530,15 +530,28 @@ pub async fn create_or_update_payment_intent(
} }
if let Some(payment_intent_id) = existing_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), amount: Some(charge_data.amount + tax_amount),
currency: Some(inferred_stripe_currency), currency: Some(inferred_stripe_currency),
customer: Some(customer_id), customer: Some(customer_id),
metadata: Some(metadata), metadata: Some(metadata),
payment_method: Some(payment_method.id.clone()),
..Default::default() ..Default::default()
}; };
// 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
{
update_payment_intent.payment_method =
Some(payment_method.id.clone());
}
stripe::PaymentIntent::update( stripe::PaymentIntent::update(
stripe_client, stripe_client,
&payment_intent_id, &payment_intent_id,