Files
AstralRinth/apps/labrinth/src/database/models/users_compliance.rs
aecsocket 17f395ee55 Mural Pay integration (#4520)
* wip: muralpay integration

* Basic Mural Pay API bindings

* Fix clippy

* use dotenvy in muralpay example

* Refactor payout creation code

* wip: muralpay payout requests

* Mural Pay payouts work

* Fix clippy

* add mural pay fees API

* Work on payout fee API

* Fees API for more payment methods

* Fix CI

* Temporarily disable Venmo and PayPal methods from frontend

* wip: counterparties

* Start on counterparties and payment methods API

* Mural Pay multiple methods when fetching

* Don't send supported_countries to frontend

* Add countries to muralpay fiat methods

* Compile fix

* Add exchange rate info to fees endpoint

* Add fees to premium Tremendous options

* Add delivery email field to Tremendous payouts

* Add Tremendous product category to payout methods

* Add bank details API to muralpay

* Fix CI

* Fix CI

* Remove prepaid visa, compute fees properly for Tremendous methods

* Add more details to Tremendous errors

* Add fees to Mural

* Payout history route and bank details

* Re-add legacy PayPal/Venmo options for US

* move the mural bank details route

* Add utoipa support to payout endpoints

* address some PR comments

* add CORS to new utoipa routes

* Immediately approve mural payouts

* Add currency support to Tremendous payouts

* Currency forex

* add forex to tremendous fee request

* Add Mural balance to bank balance info

* Add more Tremendous currencies support

* Transaction payouts available use the correct date

* Address my own review comment

* Address PR comments

* Change Mural withdrawal limit to 3k

* maybe fix tremendous gift cards

* Change how Mural minimum withdrawals are calculated

* Tweak min/max withdrawal values

---------

Co-authored-by: Calum H. <contact@cal.engineer>
Co-authored-by: Alejandro González <me@alegon.dev>
2025-11-03 14:19:46 -08:00

198 lines
5.3 KiB
Rust

use crate::database::models::DBUserId;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{query, query_scalar};
use std::fmt;
#[derive(
Debug,
Default,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
utoipa::ToSchema,
)]
pub enum FormType {
#[serde(rename = "W-8BEN")]
ForeignIndividual,
#[serde(rename = "W-8BEN-E")]
ForeignEntity,
#[default]
#[serde(rename = "W-9")]
DomesticPerson,
}
impl FormType {
pub fn as_str(&self) -> &'static str {
match self {
FormType::ForeignIndividual => "W8-BEN",
FormType::ForeignEntity => "W8-BEN-E",
FormType::DomesticPerson => "W-9",
}
}
pub fn from_str_or_default(s: &str) -> Self {
match s {
"W8-BEN" => FormType::ForeignIndividual,
"W8-BEN-E" => FormType::ForeignEntity,
"W-9" => FormType::DomesticPerson,
_ => FormType::default(),
}
}
pub fn requires_domestic_tin_match(self) -> bool {
match self {
FormType::ForeignIndividual | FormType::ForeignEntity => false,
FormType::DomesticPerson => true,
}
}
}
impl fmt::Display for FormType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug)]
pub struct UserCompliance {
pub id: i64,
pub user_id: DBUserId,
pub requested: DateTime<Utc>,
pub signed: Option<DateTime<Utc>>,
pub e_delivery_consented: bool,
pub tin_matched: bool,
pub last_checked: DateTime<Utc>,
pub external_request_id: String,
pub reference_id: String,
pub form_type: Option<FormType>,
pub requires_manual_review: bool,
}
impl UserCompliance {
pub async fn get_by_user_id<'a, E>(
exec: E,
id: DBUserId,
) -> sqlx::Result<Option<Self>>
where
E: sqlx::PgExecutor<'a>,
{
let maybe_compliance = query!(
r#"SELECT * FROM users_compliance WHERE user_id = $1"#,
id.0
)
.fetch_optional(exec)
.await?
.map(|row| UserCompliance {
id: row.id,
user_id: id,
requested: row.requested,
signed: row.signed,
e_delivery_consented: row.e_delivery_consented,
tin_matched: row.tin_matched,
last_checked: row.last_checked,
external_request_id: row.external_request_id,
reference_id: row.reference_id,
form_type: row
.form_type
.as_deref()
.map(FormType::from_str_or_default),
requires_manual_review: row.requires_manual_review,
});
Ok(maybe_compliance)
}
/// This either inserts the row into the table or updates the row, *except the requires_manual_review* column.
pub async fn upsert_partial<'a, E>(&mut self, exec: E) -> sqlx::Result<()>
where
E: sqlx::PgExecutor<'a>,
{
let id = query_scalar!(
r#"
INSERT INTO users_compliance
(
user_id,
requested,
signed,
e_delivery_consented,
tin_matched,
last_checked,
external_request_id,
reference_id,
form_type,
requires_manual_review
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
ON CONFLICT (user_id)
DO UPDATE SET
requested = EXCLUDED.requested,
signed = EXCLUDED.signed,
e_delivery_consented = EXCLUDED.e_delivery_consented,
tin_matched = EXCLUDED.tin_matched,
last_checked = EXCLUDED.last_checked,
external_request_id = EXCLUDED.external_request_id,
reference_id = EXCLUDED.reference_id,
form_type = EXCLUDED.form_type
RETURNING id
"#,
self.user_id.0,
self.requested,
self.signed,
self.e_delivery_consented,
self.tin_matched,
self.last_checked,
self.external_request_id,
self.reference_id,
self.form_type.map(|s| s.as_str()),
self.requires_manual_review,
)
.fetch_one(exec)
.await?;
self.id = id;
Ok(())
}
pub async fn update<'a, E>(&self, exec: E) -> sqlx::Result<()>
where
E: sqlx::PgExecutor<'a>,
{
query!(
r#"
UPDATE users_compliance
SET
requested = $2,
signed = $3,
e_delivery_consented = $4,
tin_matched = $5,
last_checked = $6,
external_request_id = $7,
reference_id = $8,
form_type = $9,
requires_manual_review = $10
WHERE id = $1
"#,
self.id,
self.requested,
self.signed,
self.e_delivery_consented,
self.tin_matched,
self.last_checked,
self.external_request_id,
self.reference_id,
self.form_type.map(|s| s.as_str()),
self.requires_manual_review,
)
.execute(exec)
.await?;
Ok(())
}
}