diff --git a/apps/labrinth/.sqlx/query-23fed658506cab399009f2e9ff8d092020ac9a06582a2c183c1b430b5919c6ce.json b/apps/labrinth/.sqlx/query-23fed658506cab399009f2e9ff8d092020ac9a06582a2c183c1b430b5919c6ce.json index f605a80f..878f7320 100644 --- a/apps/labrinth/.sqlx/query-23fed658506cab399009f2e9ff8d092020ac9a06582a2c183c1b430b5919c6ce.json +++ b/apps/labrinth/.sqlx/query-23fed658506cab399009f2e9ff8d092020ac9a06582a2c183c1b430b5919c6ce.json @@ -52,6 +52,11 @@ "ordinal": 9, "name": "form_type", "type_info": "Varchar" + }, + { + "ordinal": 10, + "name": "requires_manual_review", + "type_info": "Bool" } ], "parameters": { @@ -69,6 +74,7 @@ false, false, false, + true, false ] }, diff --git a/apps/labrinth/.sqlx/query-8d1f5f24360d66442dff0f1de99091bca07fcad4004451def9576dc587495d4c.json b/apps/labrinth/.sqlx/query-68ea87d071b7fca82fea5948882a5017da2fdbd81f34ab78f2ac65650880cf4d.json similarity index 73% rename from apps/labrinth/.sqlx/query-8d1f5f24360d66442dff0f1de99091bca07fcad4004451def9576dc587495d4c.json rename to apps/labrinth/.sqlx/query-68ea87d071b7fca82fea5948882a5017da2fdbd81f34ab78f2ac65650880cf4d.json index 201d39f0..cfa8308b 100644 --- a/apps/labrinth/.sqlx/query-8d1f5f24360d66442dff0f1de99091bca07fcad4004451def9576dc587495d4c.json +++ b/apps/labrinth/.sqlx/query-68ea87d071b7fca82fea5948882a5017da2fdbd81f34ab78f2ac65650880cf4d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE users_compliance\n SET\n requested = $2,\n signed = $3,\n e_delivery_consented = $4,\n tin_matched = $5,\n last_checked = $6,\n external_request_id = $7,\n reference_id = $8,\n form_type = $9\n WHERE id = $1\n ", + "query": "\n UPDATE users_compliance\n SET\n requested = $2,\n signed = $3,\n e_delivery_consented = $4,\n tin_matched = $5,\n last_checked = $6,\n external_request_id = $7,\n reference_id = $8,\n form_type = $9,\n requires_manual_review = $10\n WHERE id = $1\n ", "describe": { "columns": [], "parameters": { @@ -13,10 +13,11 @@ "Timestamptz", "Varchar", "Varchar", - "Varchar" + "Varchar", + "Bool" ] }, "nullable": [] }, - "hash": "8d1f5f24360d66442dff0f1de99091bca07fcad4004451def9576dc587495d4c" + "hash": "68ea87d071b7fca82fea5948882a5017da2fdbd81f34ab78f2ac65650880cf4d" } diff --git a/apps/labrinth/.sqlx/query-8ad3460f73020decc59106f28cdc3313ca0dc8aaf8c7b4e0f2e3a6f87ba4104b.json b/apps/labrinth/.sqlx/query-8ad3460f73020decc59106f28cdc3313ca0dc8aaf8c7b4e0f2e3a6f87ba4104b.json deleted file mode 100644 index 19210aaf..00000000 --- a/apps/labrinth/.sqlx/query-8ad3460f73020decc59106f28cdc3313ca0dc8aaf8c7b4e0f2e3a6f87ba4104b.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO users_compliance\n (\n user_id,\n requested,\n signed,\n e_delivery_consented,\n tin_matched,\n last_checked,\n external_request_id,\n reference_id,\n form_type\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n ON CONFLICT (user_id)\n DO UPDATE SET\n requested = EXCLUDED.requested,\n signed = EXCLUDED.signed,\n e_delivery_consented = EXCLUDED.e_delivery_consented,\n tin_matched = EXCLUDED.tin_matched,\n last_checked = EXCLUDED.last_checked,\n external_request_id = EXCLUDED.external_request_id,\n reference_id = EXCLUDED.reference_id,\n form_type = EXCLUDED.form_type\n RETURNING id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8", - "Timestamptz", - "Timestamptz", - "Bool", - "Bool", - "Timestamptz", - "Varchar", - "Varchar", - "Varchar" - ] - }, - "nullable": [ - false - ] - }, - "hash": "8ad3460f73020decc59106f28cdc3313ca0dc8aaf8c7b4e0f2e3a6f87ba4104b" -} diff --git a/apps/labrinth/.sqlx/query-afca60d223bee04c7e4bdfa6a4f2771f9b9c5b3fce475085e1482d751bbc7675.json b/apps/labrinth/.sqlx/query-afca60d223bee04c7e4bdfa6a4f2771f9b9c5b3fce475085e1482d751bbc7675.json new file mode 100644 index 00000000..aea45f04 --- /dev/null +++ b/apps/labrinth/.sqlx/query-afca60d223bee04c7e4bdfa6a4f2771f9b9c5b3fce475085e1482d751bbc7675.json @@ -0,0 +1,31 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO users_compliance\n (\n user_id,\n requested,\n signed,\n e_delivery_consented,\n tin_matched,\n last_checked,\n external_request_id,\n reference_id,\n form_type,\n requires_manual_review\n )\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)\n ON CONFLICT (user_id)\n DO UPDATE SET\n requested = EXCLUDED.requested,\n signed = EXCLUDED.signed,\n e_delivery_consented = EXCLUDED.e_delivery_consented,\n tin_matched = EXCLUDED.tin_matched,\n last_checked = EXCLUDED.last_checked,\n external_request_id = EXCLUDED.external_request_id,\n reference_id = EXCLUDED.reference_id,\n form_type = EXCLUDED.form_type\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Timestamptz", + "Timestamptz", + "Bool", + "Bool", + "Timestamptz", + "Varchar", + "Varchar", + "Varchar", + "Bool" + ] + }, + "nullable": [ + false + ] + }, + "hash": "afca60d223bee04c7e4bdfa6a4f2771f9b9c5b3fce475085e1482d751bbc7675" +} diff --git a/apps/labrinth/migrations/20250924064751_payout-manual.sql b/apps/labrinth/migrations/20250924064751_payout-manual.sql new file mode 100644 index 00000000..a82df6d9 --- /dev/null +++ b/apps/labrinth/migrations/20250924064751_payout-manual.sql @@ -0,0 +1,2 @@ +ALTER TABLE users_compliance ADD COLUMN requires_manual_review BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE users_compliance ALTER COLUMN form_type DROP NOT NULL; diff --git a/apps/labrinth/src/database/models/users_compliance.rs b/apps/labrinth/src/database/models/users_compliance.rs index 8fb8874c..47690ead 100644 --- a/apps/labrinth/src/database/models/users_compliance.rs +++ b/apps/labrinth/src/database/models/users_compliance.rs @@ -60,7 +60,8 @@ pub struct UserCompliance { pub last_checked: DateTime, pub external_request_id: String, pub reference_id: String, - pub form_type: FormType, + pub form_type: Option, + pub requires_manual_review: bool, } impl UserCompliance { @@ -87,13 +88,18 @@ impl UserCompliance { last_checked: row.last_checked, external_request_id: row.external_request_id, reference_id: row.reference_id, - form_type: FormType::from_str_or_default(&row.form_type), + form_type: row + .form_type + .as_deref() + .map(FormType::from_str_or_default), + requires_manual_review: row.requires_manual_review, }); Ok(maybe_compliance) } - pub async fn upsert<'a, E>(&mut self, exec: E) -> sqlx::Result<()> + /// 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>, { @@ -109,9 +115,10 @@ impl UserCompliance { last_checked, external_request_id, reference_id, - form_type + form_type, + requires_manual_review ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT (user_id) DO UPDATE SET requested = EXCLUDED.requested, @@ -132,7 +139,8 @@ impl UserCompliance { self.last_checked, self.external_request_id, self.reference_id, - self.form_type.as_str(), + self.form_type.map(|s| s.as_str()), + self.requires_manual_review, ) .fetch_one(exec) .await?; @@ -157,7 +165,8 @@ impl UserCompliance { last_checked = $6, external_request_id = $7, reference_id = $8, - form_type = $9 + form_type = $9, + requires_manual_review = $10 WHERE id = $1 "#, self.id, @@ -168,7 +177,8 @@ impl UserCompliance { self.last_checked, self.external_request_id, self.reference_id, - self.form_type.as_str(), + self.form_type.map(|s| s.as_str()), + self.requires_manual_review, ) .execute(exec) .await?; diff --git a/apps/labrinth/src/routes/v3/payouts.rs b/apps/labrinth/src/routes/v3/payouts.rs index 2d6348be..b56acb96 100644 --- a/apps/labrinth/src/routes/v3/payouts.rs +++ b/apps/labrinth/src/routes/v3/payouts.rs @@ -84,7 +84,8 @@ pub async fn post_compliance_form( reference_id: String::new(), e_delivery_consented: false, tin_matched: false, - form_type: body.0.form_type, + form_type: Some(body.0.form_type), + requires_manual_review: false, }, }; @@ -108,10 +109,10 @@ pub async fn post_compliance_form( compliance.e_delivery_consented = false; compliance.tin_matched = false; compliance.signed = None; - compliance.form_type = body.0.form_type; + compliance.form_type = Some(body.0.form_type); compliance.last_checked = Utc::now() - COMPLIANCE_CHECK_DEBOUNCE; - compliance.upsert(&mut *txn).await?; + compliance.upsert_partial(&mut *txn).await?; txn.commit().await?; Ok(HttpResponse::Ok().json(toplevel)) @@ -489,6 +490,8 @@ pub async fn create_payout( )); } + let requires_manual_review; + if let Some(threshold) = tax_compliance_payout_threshold() { let maybe_compliance = update_compliance_status(&pool, user.id).await?; @@ -501,9 +504,14 @@ pub async fn create_payout( let tin = model.tin_matched; let signed = model.signed.is_some(); + requires_manual_review = Some(model.requires_manual_review); + (tin, signed, true, compliance_api_check_failed) } - None => (false, false, false, false), + None => { + requires_manual_review = None; + (false, false, false, false) + } }; if !(tin_matched && signed) @@ -526,6 +534,22 @@ pub async fn create_payout( _ => "Tax compliance form is required to withdraw more!", }.to_owned())); } + } else { + requires_manual_review = None; + } + + let requires_manual_review = if let Some(r) = requires_manual_review { + r + } else { + users_compliance::UserCompliance::get_by_user_id(&**pool, user.id) + .await? + .is_some_and(|x| x.requires_manual_review) + }; + + if requires_manual_review { + return Err(ApiError::InvalidInput( + "More information is required to proceed. Please contact support (https://support.modrinth.com, support@modrinth.com)".to_string(), + )); } let payout_method = payouts_queue @@ -953,8 +977,9 @@ pub async fn get_balance( form_completion_status = Some( update_compliance_status(&pool, user.id.into()) .await? + .filter(|x| x.model.form_type.is_some()) .map_or(FormCompletionStatus::Unrequested, |compliance| { - requested_form_type = Some(compliance.model.form_type); + requested_form_type = compliance.model.form_type; if compliance.compliance_api_check_failed { FormCompletionStatus::Unknown @@ -1060,6 +1085,7 @@ async fn update_compliance_status( if (compliance.signed.is_some() && compliance.tin_matched) || Utc::now().signed_duration_since(compliance.last_checked) < COMPLIANCE_CHECK_DEBOUNCE + || compliance.form_type.is_none() { Ok(Some(ComplianceCheck { model: compliance, @@ -1087,7 +1113,10 @@ async fn update_compliance_status( compliance.e_delivery_consented = attributes.e_delivery_consented_at.is_some(); - if compliance.form_type.requires_domestic_tin_match() { + if compliance + .form_type + .is_some_and(|x| x.requires_domestic_tin_match()) + { compliance.tin_matched = attributes .tin_match_status .as_ref()