add: transfer refunds
This commit is contained in:
parent
95cb889080
commit
fdaae8d977
10 changed files with 340 additions and 26 deletions
|
@ -87,9 +87,9 @@
|
||||||
(span
|
(span
|
||||||
(text "{{ dislikes }}"))
|
(text "{{ dislikes }}"))
|
||||||
(text "{%- endif %}"))
|
(text "{%- endif %}"))
|
||||||
(text "{%- endif %} {%- endmacro %} {% macro full_username(user) -%} {% if user and user.username -%}")
|
(text "{%- endif %} {%- endmacro %} {% macro full_username(user, wrap=true) -%} {% if user and user.username -%}")
|
||||||
(div
|
(div
|
||||||
("class" "flex flex_wrap items_center")
|
("class" "flex {% if wrap -%} flex_wrap {%- endif %} items_center")
|
||||||
(a
|
(a
|
||||||
("href" "/@{{ user.username }}")
|
("href" "/@{{ user.username }}")
|
||||||
("class" "flush flex gap_1")
|
("class" "flush flex gap_1")
|
||||||
|
|
|
@ -49,24 +49,36 @@
|
||||||
(th (text "Sender"))
|
(th (text "Sender"))
|
||||||
(th (text "Receiver"))
|
(th (text "Receiver"))
|
||||||
(th (text "Amount"))
|
(th (text "Amount"))
|
||||||
(th (text "Product")))
|
(th (text "Product"))
|
||||||
|
(th (text "Source"))
|
||||||
|
(th (text "Actions")))
|
||||||
(tbody
|
(tbody
|
||||||
(text "{% for transfer in list -%}")
|
(text "{% for transfer in list -%}")
|
||||||
(tr
|
(tr
|
||||||
(td (span ("class" "date short") (text "{{ transfer[1] }}")))
|
(td (span ("class" "date short") (text "{{ transfer[3].created }}")))
|
||||||
(td ("class" "w_content") (text "{{ components::full_username(user=transfer[3]) }}"))
|
(td ("class" "w_content") (text "{{ components::full_username(user=transfer[0], wrap=false) }}"))
|
||||||
(td ("class" "w_content") (text "{{ components::full_username(user=transfer[4]) }}"))
|
(td ("class" "w_content") (text "{{ components::full_username(user=transfer[1], wrap=false) }}"))
|
||||||
(td
|
(td
|
||||||
("class" "flex items_center gap_1")
|
("class" "flex items_center gap_1")
|
||||||
(text "{{ transfer[2] }}")
|
(text "{{ transfer[3].amount }}")
|
||||||
(text "{% if transfer[6] -%}")
|
(text "{% if transfer[3].is_pending -%}")
|
||||||
(span ("class" "flex items_center gap_1") ("title" "Pending") (icon (text "clock")))
|
(span ("class" "flex items_center gap_1") ("title" "Pending") (icon (text "clock")))
|
||||||
(text "{%- endif %}"))
|
(text "{%- endif %}"))
|
||||||
(td
|
(td
|
||||||
(text "{% if transfer[5] -%}")
|
(text "{% if transfer[2] -%}")
|
||||||
(a
|
(a
|
||||||
("href" "/product/{{ transfer[5].id }}")
|
("href" "/product/{{ transfer[2].id }}")
|
||||||
(icon (text "external-link")))
|
(icon (text "external-link")))
|
||||||
|
(text "{%- endif %}"))
|
||||||
|
(td (text "{{ transfer[3].source }}"))
|
||||||
|
(td
|
||||||
|
(text "{% if user.id == transfer[1].id -%}")
|
||||||
|
; we're the receiver
|
||||||
|
(button
|
||||||
|
("class" "small tiny square raised camo big_icon")
|
||||||
|
("onclick" "issue_refund('{{ transfer[3].id }}')")
|
||||||
|
("title" "Issue refund")
|
||||||
|
(icon (text "undo")))
|
||||||
(text "{%- endif %}")))
|
(text "{%- endif %}")))
|
||||||
(text "{%- endfor %}")))))))
|
(text "{%- endfor %}")))))))
|
||||||
|
|
||||||
|
@ -118,5 +130,23 @@
|
||||||
window.location.href = res.payload;
|
window.location.href = res.payload;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.issue_refund = async (transfer) => {
|
||||||
|
if (
|
||||||
|
!(await trigger(\"atto::confirm\", [
|
||||||
|
\"Are you sure you would like to do this?\",
|
||||||
|
]))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/v1/transfers/${transfer}/refund`, {
|
||||||
|
method: \"POST\",
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
trigger(\"atto::toast\", [res.ok ? \"success\" : \"error\", res.message]);
|
||||||
|
});
|
||||||
}"))
|
}"))
|
||||||
(text "{% endblock %}")
|
(text "{% endblock %}")
|
||||||
|
|
|
@ -727,6 +727,10 @@ pub fn routes() -> Router {
|
||||||
// transfers
|
// transfers
|
||||||
.route("/transfers", post(transfers::create_request))
|
.route("/transfers", post(transfers::create_request))
|
||||||
.route("/transfers/ask", post(transfers::ask_request))
|
.route("/transfers/ask", post(transfers::ask_request))
|
||||||
|
.route(
|
||||||
|
"/transfers/{id}/refund",
|
||||||
|
post(transfers::create_refund_request),
|
||||||
|
)
|
||||||
// products
|
// products
|
||||||
.route("/products", post(products::create_request))
|
.route("/products", post(products::create_request))
|
||||||
.route("/products/{id}", delete(products::delete_request))
|
.route("/products/{id}", delete(products::delete_request))
|
||||||
|
@ -751,6 +755,14 @@ pub fn routes() -> Router {
|
||||||
post(products::update_method_request),
|
post(products::update_method_request),
|
||||||
)
|
)
|
||||||
.route("/products/{id}/stock", post(products::update_stock_request))
|
.route("/products/{id}/stock", post(products::update_stock_request))
|
||||||
|
.route(
|
||||||
|
"/products/{id}/uploads",
|
||||||
|
post(products::update_uploads_request),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/products/{id}/uploads/thumbnails",
|
||||||
|
delete(products::remove_thumbnail_request),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lw_routes() -> Router {
|
pub fn lw_routes() -> Router {
|
||||||
|
@ -1323,3 +1335,19 @@ pub struct AddAppliedConfiguration {
|
||||||
pub struct RemoveAppliedConfiguration {
|
pub struct RemoveAppliedConfiguration {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, PartialEq, Eq)]
|
||||||
|
pub enum ProductUploadTarget {
|
||||||
|
Thumbnails,
|
||||||
|
Reward,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdateProductUploads {
|
||||||
|
pub target: ProductUploadTarget,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct RemoveProductThumbnail {
|
||||||
|
pub idx: usize,
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
use crate::{get_user_from_token, State, cookie::CookieJar};
|
use crate::{
|
||||||
|
cookie::CookieJar,
|
||||||
|
get_user_from_token,
|
||||||
|
image::{save_webp_buffer, JsonMultipart},
|
||||||
|
State,
|
||||||
|
};
|
||||||
use axum::{extract::Path, response::IntoResponse, Extension, Json};
|
use axum::{extract::Path, response::IntoResponse, Extension, Json};
|
||||||
use tetratto_core::model::{
|
use tetratto_core::model::{
|
||||||
economy::{Product, ProductFulfillmentMethod},
|
economy::{Product, ProductFulfillmentMethod},
|
||||||
oauth,
|
oauth,
|
||||||
permissions::FinePermission,
|
permissions::FinePermission,
|
||||||
|
uploads::{MediaType, MediaUpload},
|
||||||
ApiReturn, Error,
|
ApiReturn, Error,
|
||||||
};
|
};
|
||||||
use super::{
|
use super::{
|
||||||
CreateProduct, UpdateProductData, UpdateProductDescription, UpdateProductMethod,
|
CreateProduct, ProductUploadTarget, RemoveProductThumbnail, UpdateProductData,
|
||||||
UpdateProductOnSale, UpdateProductPrice, UpdateProductSingleUse, UpdateProductStock,
|
UpdateProductDescription, UpdateProductMethod, UpdateProductOnSale, UpdateProductPrice,
|
||||||
UpdateProductTitle,
|
UpdateProductSingleUse, UpdateProductStock, UpdateProductTitle, UpdateProductUploads,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn create_request(
|
pub async fn create_request(
|
||||||
|
@ -296,3 +302,163 @@ pub async fn buy_request(
|
||||||
Err(e) => Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAXIMUM_THUMBNAIL_FILE_SIZE: usize = 2_097_152;
|
||||||
|
const MAXIMUM_REWARD_FILE_SIZE: usize = 4_194_304;
|
||||||
|
|
||||||
|
/// Update the product's uploads. Only reads one multipart file entry.
|
||||||
|
pub async fn update_uploads_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
JsonMultipart(bytes_parts, req): JsonMultipart<UpdateProductUploads>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut product = match data.get_product_by_id(id).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if user.id != product.owner && !user.permissions.check(FinePermission::MANAGE_USERS) {
|
||||||
|
return Json(Error::NotAllowed.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply to target
|
||||||
|
match req.target {
|
||||||
|
ProductUploadTarget::Thumbnails => {
|
||||||
|
if product.uploads.thumbnails.len() == 4 {
|
||||||
|
return Json(
|
||||||
|
Error::MiscError("Too many thumbnails exist. Please remove one".to_string())
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create upload
|
||||||
|
let file = match bytes_parts.get(0) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Json(Error::Unknown.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if file.len() > MAXIMUM_THUMBNAIL_FILE_SIZE {
|
||||||
|
return Json(Error::FileTooLarge.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let upload = match data
|
||||||
|
.create_upload(MediaUpload::new(MediaType::Webp, user.id))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
product.uploads.thumbnails.push(upload.id);
|
||||||
|
|
||||||
|
// write image
|
||||||
|
if let Err(e) =
|
||||||
|
save_webp_buffer(&upload.path(&data.0.0).to_string(), file.to_vec(), None)
|
||||||
|
{
|
||||||
|
return Json(Error::MiscError(e.to_string()).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ProductUploadTarget::Reward => {
|
||||||
|
// remove old
|
||||||
|
if product.uploads.reward != 0 {
|
||||||
|
if let Err(e) = data.delete_upload(product.uploads.reward).await {
|
||||||
|
return Json(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create upload
|
||||||
|
let file = match bytes_parts.get(0) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Json(Error::Unknown.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if file.len() > MAXIMUM_REWARD_FILE_SIZE {
|
||||||
|
return Json(Error::FileTooLarge.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let upload = match data
|
||||||
|
.create_upload(MediaUpload::new(MediaType::Webp, user.id))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
product.uploads.reward = upload.id;
|
||||||
|
|
||||||
|
// write image
|
||||||
|
if let Err(e) =
|
||||||
|
save_webp_buffer(&upload.path(&data.0.0).to_string(), file.to_vec(), None)
|
||||||
|
{
|
||||||
|
return Json(Error::MiscError(e.to_string()).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
match data
|
||||||
|
.update_product_uploads(id, &user, product.uploads)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Product updated".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_thumbnail_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
Json(req): Json<RemoveProductThumbnail>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut product = match data.get_product_by_id(id).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if user.id != product.owner && !user.permissions.check(FinePermission::MANAGE_USERS) {
|
||||||
|
return Json(Error::NotAllowed.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove upload
|
||||||
|
let thumbnail = match product.uploads.thumbnails.get(req.idx) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Json(Error::GeneralNotFound("thumbnail".to_string()).into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = data.delete_upload(*thumbnail).await {
|
||||||
|
return Json(e.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
product.uploads.thumbnails.remove(req.idx);
|
||||||
|
|
||||||
|
// ...
|
||||||
|
match data
|
||||||
|
.update_product_uploads(id, &user, product.uploads)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Product updated".to_string(),
|
||||||
|
payload: (),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{get_user_from_token, State, cookie::CookieJar};
|
use crate::{get_user_from_token, State, cookie::CookieJar};
|
||||||
use axum::{response::IntoResponse, Extension, Json};
|
use axum::{response::IntoResponse, Extension, Json, extract::Path};
|
||||||
use tetratto_core::model::{
|
use tetratto_core::model::{
|
||||||
economy::{CoinTransfer, CoinTransferMethod, CoinTransferSource},
|
economy::{CoinTransfer, CoinTransferMethod, CoinTransferSource},
|
||||||
oauth,
|
oauth,
|
||||||
|
@ -75,3 +75,46 @@ pub async fn ask_request(
|
||||||
Err(e) => Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_refund_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let data = &(data.read().await).0;
|
||||||
|
let user = match get_user_from_token!(jar, data, oauth::AppScope::UserSendCoins) {
|
||||||
|
Some(ua) => ua,
|
||||||
|
None => return Json(Error::NotAllowed.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let other_transfer = match data.get_transfer_by_id(id).await {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => return Json(e.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if user.id != other_transfer.receiver {
|
||||||
|
// only the receiver of the funds can issue a refund (atm)
|
||||||
|
return Json(Error::NotAllowed.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
match data
|
||||||
|
.create_transfer(
|
||||||
|
&mut CoinTransfer::new(
|
||||||
|
other_transfer.receiver,
|
||||||
|
other_transfer.sender,
|
||||||
|
other_transfer.amount,
|
||||||
|
CoinTransferMethod::Transfer,
|
||||||
|
CoinTransferSource::Refund,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(s) => Json(ApiReturn {
|
||||||
|
ok: true,
|
||||||
|
message: "Transfer created".to_string(),
|
||||||
|
payload: s.to_string(),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,5 +9,6 @@ CREATE TABLE IF NOT EXISTS products (
|
||||||
price INT NOT NULL,
|
price INT NOT NULL,
|
||||||
stock INT NOT NULL,
|
stock INT NOT NULL,
|
||||||
single_use INT NOT NULL,
|
single_use INT NOT NULL,
|
||||||
data TEXT NOT NULL
|
data TEXT NOT NULL,
|
||||||
|
uploads TEXT NOT NULL
|
||||||
)
|
)
|
||||||
|
|
|
@ -54,10 +54,14 @@ ADD COLUMN IF NOT EXISTS single_use INT DEFAULT 1;
|
||||||
ALTER TABLE transfers
|
ALTER TABLE transfers
|
||||||
ADD COLUMN IF NOT EXISTS source TEXT DEFAULT '"General"';
|
ADD COLUMN IF NOT EXISTS source TEXT DEFAULT '"General"';
|
||||||
|
|
||||||
-- products single_use
|
-- products data
|
||||||
ALTER TABLE products
|
ALTER TABLE products
|
||||||
ADD COLUMN IF NOT EXISTS data TEXT DEFAULT '';
|
ADD COLUMN IF NOT EXISTS data TEXT DEFAULT '';
|
||||||
|
|
||||||
-- users applied_configurations
|
-- users applied_configurations
|
||||||
ALTER TABLE users
|
ALTER TABLE users
|
||||||
ADD COLUMN IF NOT EXISTS applied_configurations TEXT DEFAULT '[]';
|
ADD COLUMN IF NOT EXISTS applied_configurations TEXT DEFAULT '[]';
|
||||||
|
|
||||||
|
-- products uploads
|
||||||
|
ALTER TABLE products
|
||||||
|
ADD COLUMN IF NOT EXISTS uploads TEXT DEFAULT '{}';
|
||||||
|
|
|
@ -2,6 +2,7 @@ use crate::model::{
|
||||||
auth::User,
|
auth::User,
|
||||||
economy::{
|
economy::{
|
||||||
CoinTransfer, CoinTransferMethod, CoinTransferSource, Product, ProductFulfillmentMethod,
|
CoinTransfer, CoinTransferMethod, CoinTransferSource, Product, ProductFulfillmentMethod,
|
||||||
|
ProductUploads,
|
||||||
},
|
},
|
||||||
mail::Letter,
|
mail::Letter,
|
||||||
permissions::FinePermission,
|
permissions::FinePermission,
|
||||||
|
@ -25,6 +26,7 @@ impl DataManager {
|
||||||
stock: get!(x->8(i32)),
|
stock: get!(x->8(i32)),
|
||||||
single_use: get!(x->9(i32)) as i8 == 1,
|
single_use: get!(x->9(i32)) as i8 == 1,
|
||||||
data: get!(x->10(String)),
|
data: get!(x->10(String)),
|
||||||
|
uploads: serde_json::from_str(&get!(x->11(String))).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +109,7 @@ impl DataManager {
|
||||||
|
|
||||||
let res = execute!(
|
let res = execute!(
|
||||||
&conn,
|
&conn,
|
||||||
"INSERT INTO products VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
|
"INSERT INTO products VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
|
||||||
params![
|
params![
|
||||||
&(data.id as i64),
|
&(data.id as i64),
|
||||||
&(data.created as i64),
|
&(data.created as i64),
|
||||||
|
@ -120,6 +122,7 @@ impl DataManager {
|
||||||
&(data.stock as i32),
|
&(data.stock as i32),
|
||||||
&{ if data.single_use { 1 } else { 0 } },
|
&{ if data.single_use { 1 } else { 0 } },
|
||||||
&data.data,
|
&data.data,
|
||||||
|
&serde_json::to_string(&data.uploads).unwrap(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -271,6 +274,7 @@ If your product is a purchase of goods or services, please be sure to fulfill th
|
||||||
auto_method!(update_product_method(ProductFulfillmentMethod)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET method = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.product:{}");
|
auto_method!(update_product_method(ProductFulfillmentMethod)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET method = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.product:{}");
|
||||||
auto_method!(update_product_single_use(i32)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET single_use = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
|
auto_method!(update_product_single_use(i32)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET single_use = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
|
||||||
auto_method!(update_product_data(&str)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET data = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
|
auto_method!(update_product_data(&str)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET data = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
|
||||||
|
auto_method!(update_product_uploads(ProductUploads)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET uploads = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.product:{}");
|
||||||
|
|
||||||
auto_method!(update_product_stock(i32)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET stock = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
|
auto_method!(update_product_stock(i32)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET stock = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
|
||||||
auto_method!(incr_product_stock() -> "UPDATE products SET stock = stock + 1 WHERE id = $1" --cache-key-tmpl="atto.product:{}" --incr);
|
auto_method!(incr_product_stock() -> "UPDATE products SET stock = stock + 1 WHERE id = $1" --cache-key-tmpl="atto.product:{}" --incr);
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
Error, Result,
|
|
||||||
economy::{CoinTransferMethod, Product, CoinTransfer},
|
|
||||||
auth::{Notification, User},
|
auth::{Notification, User},
|
||||||
|
economy::{CoinTransfer, CoinTransferMethod, CoinTransferSource, Product},
|
||||||
|
Error, Result,
|
||||||
};
|
};
|
||||||
use crate::{auto_method, DataManager};
|
use crate::{auto_method, DataManager};
|
||||||
use oiseau::{cache::Cache, execute, get, params, query_row, query_rows, PostgresRow};
|
use oiseau::{cache::Cache, execute, get, params, query_row, query_rows, PostgresRow};
|
||||||
|
@ -28,16 +28,13 @@ impl DataManager {
|
||||||
pub async fn fill_transfers(
|
pub async fn fill_transfers(
|
||||||
&self,
|
&self,
|
||||||
list: Vec<CoinTransfer>,
|
list: Vec<CoinTransfer>,
|
||||||
) -> Result<Vec<(usize, usize, i32, User, User, Option<Product>, bool)>> {
|
) -> Result<Vec<(User, User, Option<Product>, CoinTransfer)>> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut seen_users: HashMap<usize, User> = HashMap::new();
|
let mut seen_users: HashMap<usize, User> = HashMap::new();
|
||||||
let mut seen_products: HashMap<usize, Product> = HashMap::new();
|
let mut seen_products: HashMap<usize, Product> = HashMap::new();
|
||||||
|
|
||||||
for transfer in list {
|
for transfer in list {
|
||||||
out.push((
|
out.push((
|
||||||
transfer.id,
|
|
||||||
transfer.created,
|
|
||||||
transfer.amount,
|
|
||||||
if let Some(user) = seen_users.get(&transfer.sender) {
|
if let Some(user) = seen_users.get(&transfer.sender) {
|
||||||
user.to_owned()
|
user.to_owned()
|
||||||
} else {
|
} else {
|
||||||
|
@ -68,7 +65,7 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
transfer.is_pending,
|
transfer,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,6 +160,16 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_user_coins(receiver.id, receiver.coins).await?;
|
self.update_user_coins(receiver.id, receiver.coins).await?;
|
||||||
|
|
||||||
|
// handle refund notification
|
||||||
|
if data.source == CoinTransferSource::Refund {
|
||||||
|
self.create_notification(Notification::new(
|
||||||
|
"A coin refund has been issued to your account!".to_string(),
|
||||||
|
"You've been issued a refund for a prior purchase. The product will remain in your account, but your coins have been returned.".to_string(),
|
||||||
|
receiver.id,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// we haven't applied the transfer, so this must be pending
|
// we haven't applied the transfer, so this must be pending
|
||||||
data.is_pending = true;
|
data.is_pending = true;
|
||||||
|
|
|
@ -16,6 +16,29 @@ pub enum ProductFulfillmentMethod {
|
||||||
ProfileStyle,
|
ProfileStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ProductUploads {
|
||||||
|
/// Promotional thumbnails shown on the product page.
|
||||||
|
///
|
||||||
|
/// Maximum of 4 with a maximum upload size of 2 MiB.
|
||||||
|
#[serde(default)]
|
||||||
|
pub thumbnails: Vec<usize>,
|
||||||
|
/// Reward given to users through active configurations after they purchase the product.
|
||||||
|
//
|
||||||
|
// Maximum upload size of 4 MiB.
|
||||||
|
#[serde(default)]
|
||||||
|
pub reward: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ProductUploads {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
thumbnails: Vec::new(),
|
||||||
|
reward: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
pub struct Product {
|
pub struct Product {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
|
@ -34,9 +57,14 @@ pub struct Product {
|
||||||
/// A negative stock means the product has unlimited stock.
|
/// A negative stock means the product has unlimited stock.
|
||||||
pub stock: i32,
|
pub stock: i32,
|
||||||
/// If this product is limited to one purchase per person.
|
/// If this product is limited to one purchase per person.
|
||||||
|
#[serde(default)]
|
||||||
pub single_use: bool,
|
pub single_use: bool,
|
||||||
/// Data for this product. Only used by snippets.
|
/// Data for this product. Only used by snippets.
|
||||||
|
#[serde(default)]
|
||||||
pub data: String,
|
pub data: String,
|
||||||
|
/// Uploads for this product.
|
||||||
|
#[serde(default)]
|
||||||
|
pub uploads: ProductUploads,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Product {
|
impl Product {
|
||||||
|
@ -54,6 +82,7 @@ impl Product {
|
||||||
stock: 0,
|
stock: 0,
|
||||||
single_use: true,
|
single_use: true,
|
||||||
data: String::new(),
|
data: String::new(),
|
||||||
|
uploads: ProductUploads::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +94,7 @@ pub enum CoinTransferMethod {
|
||||||
Purchase(usize),
|
Purchase(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum CoinTransferSource {
|
pub enum CoinTransferSource {
|
||||||
/// An unknown source, such as a transfer request.
|
/// An unknown source, such as a transfer request.
|
||||||
General,
|
General,
|
||||||
|
@ -73,6 +102,8 @@ pub enum CoinTransferSource {
|
||||||
Sale,
|
Sale,
|
||||||
/// A purchase of coins through Stripe.
|
/// A purchase of coins through Stripe.
|
||||||
Purchase,
|
Purchase,
|
||||||
|
/// A refund of coins.
|
||||||
|
Refund,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue