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

@@ -15,20 +15,22 @@ pub struct DBProduct {
pub id: DBProductId,
pub metadata: ProductMetadata,
pub unitary: bool,
pub name: Option<String>,
}
struct ProductQueryResult {
id: i64,
metadata: serde_json::Value,
unitary: bool,
name: Option<String>,
}
macro_rules! select_products_with_predicate {
($predicate:tt, $param:ident) => {
($predicate:tt, $param:expr) => {
sqlx::query_as!(
ProductQueryResult,
r#"
SELECT id, metadata, unitary
SELECT products.id, products.metadata, products.unitary, products.name
FROM products
"#
+ $predicate,
@@ -45,6 +47,7 @@ impl TryFrom<ProductQueryResult> for DBProduct {
id: DBProductId(r.id),
metadata: serde_json::from_value(r.metadata)?,
unitary: r.unitary,
name: r.name,
})
}
}
@@ -57,6 +60,23 @@ impl DBProduct {
Ok(Self::get_many(&[id], exec).await?.into_iter().next())
}
pub async fn get_price(
id: DBProductPriceId,
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
) -> Result<Option<DBProduct>, DatabaseError> {
let maybe_row = select_products_with_predicate!(
"INNER JOIN products_prices pp ON pp.id = $1
WHERE products.id = pp.product_id",
id.0
)
.fetch_optional(exec)
.await?;
maybe_row
.map(|r| r.try_into().map_err(Into::into))
.transpose()
}
pub async fn get_by_type<'a, E>(
exec: E,
r#type: &str,
@@ -116,6 +136,8 @@ pub struct QueryProductWithPrices {
pub id: DBProductId,
pub metadata: ProductMetadata,
pub unitary: bool,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub name: Option<String>,
pub prices: Vec<DBProductPrice>,
}
@@ -152,6 +174,7 @@ impl QueryProductWithPrices {
Some(QueryProductWithPrices {
id: x.id,
metadata: x.metadata,
name: x.name,
prices: prices
.remove(&x.id)
.map(|x| x.1)?
@@ -195,6 +218,7 @@ impl QueryProductWithPrices {
Some(QueryProductWithPrices {
id: x.id,
metadata: x.metadata,
name: x.name,
prices: prices
.remove(&x.id)
.map(|x| x.1)?