Add public column to products prices, only expose public prices
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
ALTER TABLE
|
||||||
|
products_prices
|
||||||
|
ADD COLUMN
|
||||||
|
public BOOLEAN NOT NULL DEFAULT true;
|
||||||
@@ -100,7 +100,8 @@ pub struct QueryProductWithPrices {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl QueryProductWithPrices {
|
impl QueryProductWithPrices {
|
||||||
pub async fn list<'a, E>(
|
/// Lists products with at least one public price.
|
||||||
|
pub async fn list_purchaseable<'a, E>(
|
||||||
exec: E,
|
exec: E,
|
||||||
redis: &RedisPool,
|
redis: &RedisPool,
|
||||||
) -> Result<Vec<QueryProductWithPrices>, DatabaseError>
|
) -> Result<Vec<QueryProductWithPrices>, DatabaseError>
|
||||||
@@ -118,30 +119,32 @@ impl QueryProductWithPrices {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let all_products = product_item::DBProduct::get_all(exec).await?;
|
let all_products = product_item::DBProduct::get_all(exec).await?;
|
||||||
let prices = product_item::DBProductPrice::get_all_products_prices(
|
let prices =
|
||||||
&all_products.iter().map(|x| x.id).collect::<Vec<_>>(),
|
product_item::DBProductPrice::get_all_public_products_prices(
|
||||||
exec,
|
&all_products.iter().map(|x| x.id).collect::<Vec<_>>(),
|
||||||
)
|
exec,
|
||||||
.await?;
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let products = all_products
|
let products = all_products
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| QueryProductWithPrices {
|
.filter_map(|x| {
|
||||||
id: x.id,
|
Some(QueryProductWithPrices {
|
||||||
metadata: x.metadata,
|
id: x.id,
|
||||||
prices: prices
|
metadata: x.metadata,
|
||||||
.remove(&x.id)
|
prices: prices
|
||||||
.map(|x| x.1)
|
.remove(&x.id)
|
||||||
.unwrap_or_default()
|
.map(|x| x.1)?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|x| DBProductPrice {
|
.map(|x| DBProductPrice {
|
||||||
id: x.id,
|
id: x.id,
|
||||||
product_id: x.product_id,
|
product_id: x.product_id,
|
||||||
prices: x.prices,
|
prices: x.prices,
|
||||||
currency_code: x.currency_code,
|
currency_code: x.currency_code,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
unitary: x.unitary,
|
unitary: x.unitary,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
@@ -222,16 +225,19 @@ impl DBProductPrice {
|
|||||||
.collect::<Result<Vec<_>, serde_json::Error>>()?)
|
.collect::<Result<Vec<_>, serde_json::Error>>()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all_product_prices(
|
pub async fn get_all_public_product_prices(
|
||||||
product_id: DBProductId,
|
product_id: DBProductId,
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||||
) -> Result<Vec<DBProductPrice>, DatabaseError> {
|
) -> Result<Vec<DBProductPrice>, DatabaseError> {
|
||||||
let res = Self::get_all_products_prices(&[product_id], exec).await?;
|
let res =
|
||||||
|
Self::get_all_public_products_prices(&[product_id], exec).await?;
|
||||||
|
|
||||||
Ok(res.remove(&product_id).map(|x| x.1).unwrap_or_default())
|
Ok(res.remove(&product_id).map(|x| x.1).unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all_products_prices(
|
/// Gets all public prices for the given products. If a product has no public price,
|
||||||
|
/// it won't be included in the resulting map.
|
||||||
|
pub async fn get_all_public_products_prices(
|
||||||
product_ids: &[DBProductId],
|
product_ids: &[DBProductId],
|
||||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||||
) -> Result<DashMap<DBProductId, Vec<DBProductPrice>>, DatabaseError> {
|
) -> Result<DashMap<DBProductId, Vec<DBProductPrice>>, DatabaseError> {
|
||||||
@@ -240,7 +246,8 @@ impl DBProductPrice {
|
|||||||
|
|
||||||
use futures_util::TryStreamExt;
|
use futures_util::TryStreamExt;
|
||||||
let prices = select_prices_with_predicate!(
|
let prices = select_prices_with_predicate!(
|
||||||
"WHERE product_id = ANY($1::bigint[])",
|
"WHERE product_id = ANY($1::bigint[])
|
||||||
|
AND public = true",
|
||||||
ids_ref
|
ids_ref
|
||||||
)
|
)
|
||||||
.fetch(exec)
|
.fetch(exec)
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ impl RedeemalLookupFields {
|
|||||||
///
|
///
|
||||||
/// If the returned value is `Ok(Some(fields))`, but `redeemal_status` is `None`,
|
/// If the returned value is `Ok(Some(fields))`, but `redeemal_status` is `None`,
|
||||||
/// the user exists and has not redeemed the offer.
|
/// the user exists and has not redeemed the offer.
|
||||||
pub async fn redeemal_status_by_user_username_and_offer<'a, E>(
|
pub async fn redeemal_status_by_username_and_offer<'a, E>(
|
||||||
exec: E,
|
exec: E,
|
||||||
user_username: &str,
|
user_username: &str,
|
||||||
offer: Offer,
|
offer: Offer,
|
||||||
@@ -183,7 +183,7 @@ impl RedeemalLookupFields {
|
|||||||
redeemal_status: row
|
redeemal_status: row
|
||||||
.status
|
.status
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|s| Status::from_str_or_default(s)),
|
.map(Status::from_str_or_default),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,8 +59,10 @@ pub async fn products(
|
|||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
redis: web::Data<RedisPool>,
|
redis: web::Data<RedisPool>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let products =
|
let products = product_item::QueryProductWithPrices::list_purchaseable(
|
||||||
product_item::QueryProductWithPrices::list(&**pool, &redis).await?;
|
&**pool, &redis,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let products = products
|
let products = products
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -408,7 +410,7 @@ pub async fn edit_subscription(
|
|||||||
|
|
||||||
let intent = if let Some(product_id) = &edit_subscription.product {
|
let intent = if let Some(product_id) = &edit_subscription.product {
|
||||||
let product_price =
|
let product_price =
|
||||||
product_item::DBProductPrice::get_all_product_prices(
|
product_item::DBProductPrice::get_all_public_product_prices(
|
||||||
(*product_id).into(),
|
(*product_id).into(),
|
||||||
&mut *transaction,
|
&mut *transaction,
|
||||||
)
|
)
|
||||||
@@ -1187,7 +1189,7 @@ pub async fn initiate_payment(
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let mut product_prices =
|
let mut product_prices =
|
||||||
product_item::DBProductPrice::get_all_product_prices(
|
product_item::DBProductPrice::get_all_public_product_prices(
|
||||||
product.id, &**pool,
|
product.id, &**pool,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
use actix_web::{HttpRequest, HttpResponse, post, web};
|
use actix_web::{HttpResponse, post, web};
|
||||||
use ariadne::ids::UserId;
|
use ariadne::ids::UserId;
|
||||||
|
use chrono::Utc;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
use crate::database::models::users_redeemals::{Offer, RedeemalLookupFields};
|
use crate::database::models::users_redeemals::{
|
||||||
|
Offer, RedeemalLookupFields, Status, UserRedeemal,
|
||||||
|
};
|
||||||
use crate::routes::ApiError;
|
use crate::routes::ApiError;
|
||||||
use crate::util::guards::medal_key_guard;
|
use crate::util::guards::medal_key_guard;
|
||||||
|
|
||||||
@@ -12,18 +15,17 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct MedalVerifyQuery {
|
struct MedalQuery {
|
||||||
username: String,
|
username: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("verify", guard = "medal_key_guard")]
|
#[post("verify", guard = "medal_key_guard")]
|
||||||
pub async fn verify(
|
pub async fn verify(
|
||||||
_req: HttpRequest,
|
|
||||||
pool: web::Data<PgPool>,
|
pool: web::Data<PgPool>,
|
||||||
web::Query(MedalVerifyQuery { username }): web::Query<MedalVerifyQuery>,
|
web::Query(MedalQuery { username }): web::Query<MedalQuery>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
let maybe_fields =
|
let maybe_fields =
|
||||||
RedeemalLookupFields::redeemal_status_by_user_username_and_offer(
|
RedeemalLookupFields::redeemal_status_by_username_and_offer(
|
||||||
&**pool,
|
&**pool,
|
||||||
&username,
|
&username,
|
||||||
Offer::Medal,
|
Offer::Medal,
|
||||||
@@ -47,8 +49,44 @@ pub async fn verify(
|
|||||||
|
|
||||||
#[post("redeem", guard = "medal_key_guard")]
|
#[post("redeem", guard = "medal_key_guard")]
|
||||||
pub async fn redeem(
|
pub async fn redeem(
|
||||||
_req: HttpRequest,
|
pool: web::Data<PgPool>,
|
||||||
_pool: web::Data<PgPool>,
|
web::Query(MedalQuery { username }): web::Query<MedalQuery>,
|
||||||
) -> Result<HttpResponse, ApiError> {
|
) -> Result<HttpResponse, ApiError> {
|
||||||
Ok(HttpResponse::NotImplemented().finish())
|
// Check the offer hasn't been redeemed yet, then insert into the table.
|
||||||
|
|
||||||
|
let mut txn = pool.begin().await?;
|
||||||
|
|
||||||
|
let maybe_fields =
|
||||||
|
RedeemalLookupFields::redeemal_status_by_username_and_offer(
|
||||||
|
&mut *txn,
|
||||||
|
&username,
|
||||||
|
Offer::Medal,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let _redeemal = 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(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut redeemal = UserRedeemal {
|
||||||
|
id: 0,
|
||||||
|
user_id: fields.user_id,
|
||||||
|
offer: Offer::Medal,
|
||||||
|
redeemed: Utc::now(),
|
||||||
|
status: Status::Pending,
|
||||||
|
};
|
||||||
|
|
||||||
|
redeemal.insert(&mut *txn).await?;
|
||||||
|
redeemal
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
txn.commit().await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().finish())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ pub enum ApiError {
|
|||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
#[error("Resource not found")]
|
#[error("Resource not found")]
|
||||||
NotFound,
|
NotFound,
|
||||||
|
#[error("Conflict: {0}")]
|
||||||
|
Conflict(String),
|
||||||
#[error(
|
#[error(
|
||||||
"You are being rate-limited. Please wait {0} milliseconds. 0/{1} remaining."
|
"You are being rate-limited. Please wait {0} milliseconds. 0/{1} remaining."
|
||||||
)]
|
)]
|
||||||
@@ -172,6 +174,7 @@ impl ApiError {
|
|||||||
ApiError::Clickhouse(..) => "clickhouse_error",
|
ApiError::Clickhouse(..) => "clickhouse_error",
|
||||||
ApiError::Reroute(..) => "reroute_error",
|
ApiError::Reroute(..) => "reroute_error",
|
||||||
ApiError::NotFound => "not_found",
|
ApiError::NotFound => "not_found",
|
||||||
|
ApiError::Conflict(..) => "conflict",
|
||||||
ApiError::Zip(..) => "zip_error",
|
ApiError::Zip(..) => "zip_error",
|
||||||
ApiError::Io(..) => "io_error",
|
ApiError::Io(..) => "io_error",
|
||||||
ApiError::RateLimitError(..) => "ratelimit_error",
|
ApiError::RateLimitError(..) => "ratelimit_error",
|
||||||
@@ -208,6 +211,7 @@ impl actix_web::ResponseError for ApiError {
|
|||||||
ApiError::Mail(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
ApiError::Mail(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
ApiError::Reroute(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
ApiError::Reroute(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
ApiError::NotFound => StatusCode::NOT_FOUND,
|
ApiError::NotFound => StatusCode::NOT_FOUND,
|
||||||
|
ApiError::Conflict(..) => StatusCode::CONFLICT,
|
||||||
ApiError::Zip(..) => StatusCode::BAD_REQUEST,
|
ApiError::Zip(..) => StatusCode::BAD_REQUEST,
|
||||||
ApiError::Io(..) => StatusCode::BAD_REQUEST,
|
ApiError::Io(..) => StatusCode::BAD_REQUEST,
|
||||||
ApiError::RateLimitError(..) => StatusCode::TOO_MANY_REQUESTS,
|
ApiError::RateLimitError(..) => StatusCode::TOO_MANY_REQUESTS,
|
||||||
|
|||||||
Reference in New Issue
Block a user