From 98b4970680462e0e5472ae925ee8040cd7a8a83f Mon Sep 17 00:00:00 2001 From: aecsocket Date: Mon, 10 Nov 2025 08:41:06 -0800 Subject: [PATCH] Store method ID for payouts (#4752) * Store method ID for payouts * Fixes --- ...7c70a245b0cc48ae3f4883393bfd57a685f1.json} | 14 ++- ...1049791dab817ac07708873ac57986c17e2c.json} | 5 +- ...0251108210105_add_method_id_to_payouts.sql | 2 + .../src/database/models/payout_item.rs | 12 ++- apps/labrinth/src/models/v3/payouts.rs | 12 ++- apps/labrinth/src/routes/v3/payouts.rs | 102 ++++++++++-------- 6 files changed, 95 insertions(+), 52 deletions(-) rename apps/labrinth/.sqlx/{query-83f8d3fcc4ba1544f593abaf29c79157bc35e3fca79cc93f6512ca01acd8e5ce.json => query-2aa9704c9ead520fb61b4ca1e94c7c70a245b0cc48ae3f4883393bfd57a685f1.json} (79%) rename apps/labrinth/.sqlx/{query-285c089b43bf0225ba03e279f7a227c3483bae818d077efdc54e588b858c8760.json => query-c82bf13a2567f772a4c6eb3329971049791dab817ac07708873ac57986c17e2c.json} (53%) create mode 100644 apps/labrinth/migrations/20251108210105_add_method_id_to_payouts.sql diff --git a/apps/labrinth/.sqlx/query-83f8d3fcc4ba1544f593abaf29c79157bc35e3fca79cc93f6512ca01acd8e5ce.json b/apps/labrinth/.sqlx/query-2aa9704c9ead520fb61b4ca1e94c7c70a245b0cc48ae3f4883393bfd57a685f1.json similarity index 79% rename from apps/labrinth/.sqlx/query-83f8d3fcc4ba1544f593abaf29c79157bc35e3fca79cc93f6512ca01acd8e5ce.json rename to apps/labrinth/.sqlx/query-2aa9704c9ead520fb61b4ca1e94c7c70a245b0cc48ae3f4883393bfd57a685f1.json index ac2af681..d0dabd44 100644 --- a/apps/labrinth/.sqlx/query-83f8d3fcc4ba1544f593abaf29c79157bc35e3fca79cc93f6512ca01acd8e5ce.json +++ b/apps/labrinth/.sqlx/query-2aa9704c9ead520fb61b4ca1e94c7c70a245b0cc48ae3f4883393bfd57a685f1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id, user_id, created, amount, status, method, method_address, platform_id, fee\n FROM payouts\n WHERE id = ANY($1)\n ", + "query": "\n SELECT id, user_id, created, amount, status, method, method_id, method_address, platform_id, fee\n FROM payouts\n WHERE id = ANY($1)\n ", "describe": { "columns": [ { @@ -35,16 +35,21 @@ }, { "ordinal": 6, - "name": "method_address", + "name": "method_id", "type_info": "Text" }, { "ordinal": 7, - "name": "platform_id", + "name": "method_address", "type_info": "Text" }, { "ordinal": 8, + "name": "platform_id", + "type_info": "Text" + }, + { + "ordinal": 9, "name": "fee", "type_info": "Numeric" } @@ -63,8 +68,9 @@ true, true, true, + true, true ] }, - "hash": "83f8d3fcc4ba1544f593abaf29c79157bc35e3fca79cc93f6512ca01acd8e5ce" + "hash": "2aa9704c9ead520fb61b4ca1e94c7c70a245b0cc48ae3f4883393bfd57a685f1" } diff --git a/apps/labrinth/.sqlx/query-285c089b43bf0225ba03e279f7a227c3483bae818d077efdc54e588b858c8760.json b/apps/labrinth/.sqlx/query-c82bf13a2567f772a4c6eb3329971049791dab817ac07708873ac57986c17e2c.json similarity index 53% rename from apps/labrinth/.sqlx/query-285c089b43bf0225ba03e279f7a227c3483bae818d077efdc54e588b858c8760.json rename to apps/labrinth/.sqlx/query-c82bf13a2567f772a4c6eb3329971049791dab817ac07708873ac57986c17e2c.json index 0b54267b..44fbfc3a 100644 --- a/apps/labrinth/.sqlx/query-285c089b43bf0225ba03e279f7a227c3483bae818d077efdc54e588b858c8760.json +++ b/apps/labrinth/.sqlx/query-c82bf13a2567f772a4c6eb3329971049791dab817ac07708873ac57986c17e2c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO payouts (\n id, amount, fee, user_id, status, method, method_address, platform_id\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8\n )\n ", + "query": "\n INSERT INTO payouts (\n id, amount, fee, user_id, status, method, method_id, method_address, platform_id\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9\n )\n ", "describe": { "columns": [], "parameters": { @@ -12,10 +12,11 @@ "Varchar", "Text", "Text", + "Text", "Text" ] }, "nullable": [] }, - "hash": "285c089b43bf0225ba03e279f7a227c3483bae818d077efdc54e588b858c8760" + "hash": "c82bf13a2567f772a4c6eb3329971049791dab817ac07708873ac57986c17e2c" } diff --git a/apps/labrinth/migrations/20251108210105_add_method_id_to_payouts.sql b/apps/labrinth/migrations/20251108210105_add_method_id_to_payouts.sql new file mode 100644 index 00000000..47ae8b83 --- /dev/null +++ b/apps/labrinth/migrations/20251108210105_add_method_id_to_payouts.sql @@ -0,0 +1,2 @@ +ALTER TABLE payouts +ADD COLUMN method_id TEXT; diff --git a/apps/labrinth/src/database/models/payout_item.rs b/apps/labrinth/src/database/models/payout_item.rs index d8d82b05..102f4f4d 100644 --- a/apps/labrinth/src/database/models/payout_item.rs +++ b/apps/labrinth/src/database/models/payout_item.rs @@ -15,7 +15,11 @@ pub struct DBPayout { pub fee: Option, pub method: Option, + /// See [`crate::models::v3::payouts::Payout::method_id`]. + pub method_id: Option, + /// See [`crate::models::v3::payouts::Payout::method_address`]. pub method_address: Option, + /// See [`crate::models::v3::payouts::Payout::platform_id`]. pub platform_id: Option, } @@ -27,10 +31,10 @@ impl DBPayout { sqlx::query!( " INSERT INTO payouts ( - id, amount, fee, user_id, status, method, method_address, platform_id + id, amount, fee, user_id, status, method, method_id, method_address, platform_id ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8 + $1, $2, $3, $4, $5, $6, $7, $8, $9 ) ", self.id.0, @@ -39,6 +43,7 @@ impl DBPayout { self.user_id.0, self.status.as_str(), self.method.as_ref().map(|x| x.as_str()), + self.method_id, self.method_address, self.platform_id, ) @@ -71,7 +76,7 @@ impl DBPayout { let results = sqlx::query!( " - SELECT id, user_id, created, amount, status, method, method_address, platform_id, fee + SELECT id, user_id, created, amount, status, method, method_id, method_address, platform_id, fee FROM payouts WHERE id = ANY($1) ", @@ -85,6 +90,7 @@ impl DBPayout { status: PayoutStatus::from_string(&r.status), amount: r.amount, method: r.method.and_then(|x| PayoutMethodType::from_string(&x)), + method_id: r.method_id, method_address: r.method_address, platform_id: r.platform_id, fee: r.fee, diff --git a/apps/labrinth/src/models/v3/payouts.rs b/apps/labrinth/src/models/v3/payouts.rs index 24330525..46af53f6 100644 --- a/apps/labrinth/src/models/v3/payouts.rs +++ b/apps/labrinth/src/models/v3/payouts.rs @@ -18,8 +18,17 @@ pub struct Payout { #[serde(with = "rust_decimal::serde::float_option")] pub fee: Option, pub method: Option, - /// the address this payout was sent to: ex: email, paypal email, venmo handle + /// Platform-dependent identifier for the submethod. + /// + /// See [`crate::routes::v3::payouts::TransactionItem::Withdrawal::method_id`]. + pub method_id: Option, + /// Address this payout was sent to: ex: email, paypal email, venmo handle. pub method_address: Option, + /// Platform-provided opaque identifier for the transaction linked to this payout. + /// + /// - Tremendous: reward ID + /// - Mural: payout request UUID + /// - PayPal/Venmo: transaction ID pub platform_id: Option, } @@ -33,6 +42,7 @@ impl Payout { amount: data.amount, fee: data.fee, method: data.method, + method_id: data.method_id, method_address: data.method_address, platform_id: data.platform_id, } diff --git a/apps/labrinth/src/routes/v3/payouts.rs b/apps/labrinth/src/routes/v3/payouts.rs index 00384e92..3a33f771 100644 --- a/apps/labrinth/src/routes/v3/payouts.rs +++ b/apps/labrinth/src/routes/v3/payouts.rs @@ -10,6 +10,7 @@ use crate::models::payouts::{ MuralPayDetails, PayoutMethodRequest, PayoutMethodType, PayoutStatus, TremendousDetails, TremendousForexResponse, }; +use crate::queue::payouts::mural::MuralPayoutRequest; use crate::queue::payouts::{PayoutFees, PayoutsQueue}; use crate::queue::session::AuthQueue; use crate::routes::ApiError; @@ -772,6 +773,7 @@ async fn tremendous_payout( amount: amount_minus_fee, fee: Some(total_fee), method: Some(PayoutMethodType::Tremendous), + method_id: Some(body.method_id.clone()), method_address: Some(user_email.to_string()), platform_id, }) @@ -806,6 +808,16 @@ async fn mural_pay_payout( ) .await?; + let method_id = match &details.payout_details { + MuralPayoutRequest::Blockchain { .. } => { + "blockchain-usdc-polygon".to_string() + } + MuralPayoutRequest::Fiat { + fiat_and_rail_details, + .. + } => fiat_and_rail_details.code().to_string(), + }; + Ok(DBPayout { id: payout_id, user_id: user.id, @@ -814,6 +826,7 @@ async fn mural_pay_payout( amount: amount_minus_fee, fee: Some(total_fee), method: Some(PayoutMethodType::MuralPay), + method_id: Some(method_id), method_address: Some(user_email.to_string()), platform_id: Some(payout_request.id.to_string()), }) @@ -883,18 +896,6 @@ async fn paypal_payout( pub links: Vec, } - let mut payout_item = crate::database::models::payout_item::DBPayout { - id: payout_id, - user_id: user.id, - created: Utc::now(), - status: PayoutStatus::InTransit, - amount: amount_minus_fee, - fee: Some(total_fee), - method: Some(body.method.method_type()), - method_address: Some(display_address.clone()), - platform_id: None, - }; - let res: PayoutsResponse = payouts_queue.make_paypal_request( Method::POST, "payments/payouts", @@ -922,33 +923,50 @@ async fn paypal_payout( None ).await?; - if let Some(link) = res.links.first() { - #[derive(Deserialize)] - struct PayoutItem { - pub payout_item_id: String, - } + let link = res + .links + .first() + .wrap_request_err("no PayPal links available")?; - #[derive(Deserialize)] - struct PayoutData { - pub items: Vec, - } - - if let Ok(res) = payouts_queue - .make_paypal_request::<(), PayoutData>( - Method::GET, - &link.href, - None, - None, - Some(true), - ) - .await - && let Some(data) = res.items.first() - { - payout_item.platform_id = Some(data.payout_item_id.clone()); - } + #[derive(Deserialize)] + struct PayoutItem { + pub payout_item_id: String, } - Ok(payout_item) + #[derive(Deserialize)] + struct PayoutData { + pub items: Vec, + } + + let res = payouts_queue + .make_paypal_request::<(), PayoutData>( + Method::GET, + &link.href, + None, + None, + Some(true), + ) + .await + .wrap_internal_err("failed to make PayPal request")?; + let data = res + .items + .first() + .wrap_internal_err("no payout items returned from PayPal request")?; + + let platform_id = Some(data.payout_item_id.clone()); + + Ok(DBPayout { + id: payout_id, + user_id: user.id, + created: Utc::now(), + status: PayoutStatus::InTransit, + amount: amount_minus_fee, + fee: Some(total_fee), + method: Some(body.method.method_type()), + method_id: Some(body.method_id.clone()), + method_address: Some(display_address.clone()), + platform_id, + }) } /// User performing a payout-related action. @@ -972,9 +990,11 @@ pub enum TransactionItem { /// Payout-method-specific ID for the type of payout the user got. /// /// - Tremendous: the rewarded gift card ID. - /// - Mural: the payment rail used, i.e. crypto USDC or fiat USD. - /// - PayPal: `paypal_us` - /// - Venmo: `venmo` + /// - Mural: the payment rail code used. + /// - Blockchain: `blockchain-usdc-polygon`. + /// - Fiat: see [`muralpay::FiatAndRailCode`]. + /// - PayPal: `paypal_us`. + /// - Venmo: `venmo`. /// /// For legacy transactions, this may be [`None`] as we did not always /// store this payout info. @@ -1061,9 +1081,7 @@ pub async fn transaction_history( amount: payout.amount, fee: payout.fee, method_type: payout.method, - // TODO: store the `method_id` in the database, and return it here - // don't use the `platform_id`, that's something else - method_id: None, + method_id: payout.method_id, method_address: payout.method_address, });