Files
AstralRinth/apps/labrinth/src/routes/internal/medal.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

110 lines
2.9 KiB
Rust

use actix_web::{HttpResponse, post, web};
use ariadne::ids::UserId;
use chrono::Utc;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use tracing::warn;
use crate::database::models::users_redeemals::{
Offer, RedeemalLookupFields, Status, UserRedeemal,
};
use crate::database::redis::RedisPool;
use crate::queue::billing::try_process_user_redeemal;
use crate::routes::ApiError;
use crate::util::guards::medal_key_guard;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(web::scope("medal").service(verify).service(redeem));
}
#[derive(Deserialize)]
struct MedalQuery {
username: String,
}
#[post("verify", guard = "medal_key_guard")]
pub async fn verify(
pool: web::Data<PgPool>,
web::Query(MedalQuery { username }): web::Query<MedalQuery>,
) -> Result<HttpResponse, ApiError> {
let maybe_fields =
RedeemalLookupFields::redeemal_status_by_username_and_offer(
&**pool,
&username,
Offer::Medal,
)
.await?;
#[derive(Serialize)]
struct VerifyResponse {
user_id: UserId,
redeemed: bool,
}
match maybe_fields {
None => Err(ApiError::NotFound),
Some(fields) => Ok(HttpResponse::Ok().json(VerifyResponse {
user_id: fields.user_id.into(),
redeemed: fields.redeemal_status.is_some(),
})),
}
}
#[post("redeem", guard = "medal_key_guard")]
pub async fn redeem(
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
web::Query(MedalQuery { username }): web::Query<MedalQuery>,
) -> Result<HttpResponse, ApiError> {
// Check the offer hasn't been redeemed yet, then insert into the table.
// In a transaction to avoid double inserts.
let mut txn = pool.begin().await?;
let maybe_fields =
RedeemalLookupFields::redeemal_status_by_username_and_offer(
&mut *txn,
&username,
Offer::Medal,
)
.await?;
let user_id = match maybe_fields {
None => return Err(ApiError::NotFound),
Some(fields) => {
if fields.redeemal_status.is_some() {
return Err(ApiError::Conflict(
"User already redeemed this offer".to_string(),
));
}
fields.user_id
}
};
// Link user to offer redeemal.
let mut redeemal = UserRedeemal {
id: 0,
user_id,
offer: Offer::Medal,
redeemed: Utc::now(),
status: Status::Pending,
last_attempt: None,
n_attempts: 0,
};
redeemal.insert(&mut *txn).await?;
txn.commit().await?;
// Immediately try to process the redeemal
if let Err(error) = try_process_user_redeemal(&pool, &redis, redeemal).await
{
warn!(%error, "Medal redeemal processing failed");
Ok(HttpResponse::Accepted().finish())
} else {
Ok(HttpResponse::Created().finish())
}
}