diff --git a/apps/labrinth/.sqlx/query-043f8c91cc63fefaf56b4d59f1b46addf63c85056d9bc47f1f7a4eba86b914cd.json b/apps/labrinth/.sqlx/query-043f8c91cc63fefaf56b4d59f1b46addf63c85056d9bc47f1f7a4eba86b914cd.json new file mode 100644 index 000000000..f68de218d --- /dev/null +++ b/apps/labrinth/.sqlx/query-043f8c91cc63fefaf56b4d59f1b46addf63c85056d9bc47f1f7a4eba86b914cd.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT id, mod_id FROM mods_gallery\n WHERE image_url = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "mod_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "043f8c91cc63fefaf56b4d59f1b46addf63c85056d9bc47f1f7a4eba86b914cd" +} diff --git a/apps/labrinth/.sqlx/query-87a0f2f991d749876d90b8e4ab73727f638a019b64e6cb1d891b333c2f09099c.json b/apps/labrinth/.sqlx/query-1cedeb3367e780314d99b4c069c5a98383277e0db6240394c9a36bbf5fd5d597.json similarity index 61% rename from apps/labrinth/.sqlx/query-87a0f2f991d749876d90b8e4ab73727f638a019b64e6cb1d891b333c2f09099c.json rename to apps/labrinth/.sqlx/query-1cedeb3367e780314d99b4c069c5a98383277e0db6240394c9a36bbf5fd5d597.json index 57038f239..b55505eed 100644 --- a/apps/labrinth/.sqlx/query-87a0f2f991d749876d90b8e4ab73727f638a019b64e6cb1d891b333c2f09099c.json +++ b/apps/labrinth/.sqlx/query-1cedeb3367e780314d99b4c069c5a98383277e0db6240394c9a36bbf5fd5d597.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id, image_url, raw_image_url FROM mods_gallery\n WHERE image_url = $1\n ", + "query": "\n SELECT id, image_url, raw_image_url, mod_id FROM mods_gallery\n WHERE image_url = $1\n ", "describe": { "columns": [ { @@ -17,6 +17,11 @@ "ordinal": 2, "name": "raw_image_url", "type_info": "Text" + }, + { + "ordinal": 3, + "name": "mod_id", + "type_info": "Int8" } ], "parameters": { @@ -25,10 +30,11 @@ ] }, "nullable": [ + false, false, false, false ] }, - "hash": "87a0f2f991d749876d90b8e4ab73727f638a019b64e6cb1d891b333c2f09099c" + "hash": "1cedeb3367e780314d99b4c069c5a98383277e0db6240394c9a36bbf5fd5d597" } diff --git a/apps/labrinth/.sqlx/query-86ee460c74f0052a4945ab4df9829b3b077930d8e9e09dca76fde8983413adc6.json b/apps/labrinth/.sqlx/query-4d1e510cc4339da9f229f8708863321b9b6ceb84ace27ab3be29448a2572072c.json similarity index 67% rename from apps/labrinth/.sqlx/query-86ee460c74f0052a4945ab4df9829b3b077930d8e9e09dca76fde8983413adc6.json rename to apps/labrinth/.sqlx/query-4d1e510cc4339da9f229f8708863321b9b6ceb84ace27ab3be29448a2572072c.json index e146de6b9..006571836 100644 --- a/apps/labrinth/.sqlx/query-86ee460c74f0052a4945ab4df9829b3b077930d8e9e09dca76fde8983413adc6.json +++ b/apps/labrinth/.sqlx/query-4d1e510cc4339da9f229f8708863321b9b6ceb84ace27ab3be29448a2572072c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval\n FROM charges\n WHERE id = $1", + "query": "\n SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval, payment_platform, payment_platform_id, net, refunded\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled')", "describe": { "columns": [ { @@ -57,6 +57,26 @@ "ordinal": 10, "name": "subscription_interval", "type_info": "Text" + }, + { + "ordinal": 11, + "name": "payment_platform", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "payment_platform_id", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "net", + "type_info": "Int8" + }, + { + "ordinal": 14, + "name": "refunded", + "type_info": "Int8" } ], "parameters": { @@ -75,8 +95,12 @@ true, false, true, - true + true, + false, + true, + false, + false ] }, - "hash": "86ee460c74f0052a4945ab4df9829b3b077930d8e9e09dca76fde8983413adc6" + "hash": "4d1e510cc4339da9f229f8708863321b9b6ceb84ace27ab3be29448a2572072c" } diff --git a/apps/labrinth/.sqlx/query-56d7b62fc05c77f228e46dbfe4eaca81b445a7f5a44e52a0526a1b57bd7a8c9d.json b/apps/labrinth/.sqlx/query-56d7b62fc05c77f228e46dbfe4eaca81b445a7f5a44e52a0526a1b57bd7a8c9d.json deleted file mode 100644 index 53bd4798f..000000000 --- a/apps/labrinth/.sqlx/query-56d7b62fc05c77f228e46dbfe4eaca81b445a7f5a44e52a0526a1b57bd7a8c9d.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)\n ON CONFLICT (id)\n DO UPDATE\n SET status = EXCLUDED.status,\n last_attempt = EXCLUDED.last_attempt,\n due = EXCLUDED.due,\n subscription_id = EXCLUDED.subscription_id,\n subscription_interval = EXCLUDED.subscription_interval\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Int8", - "Int8", - "Text", - "Text", - "Varchar", - "Timestamptz", - "Timestamptz", - "Int8", - "Text" - ] - }, - "nullable": [] - }, - "hash": "56d7b62fc05c77f228e46dbfe4eaca81b445a7f5a44e52a0526a1b57bd7a8c9d" -} diff --git a/apps/labrinth/.sqlx/query-e68e27fcb3e85233be06e7435aaeb6b27d8dbe2ddaf211ba37a026eab3bb6926.json b/apps/labrinth/.sqlx/query-5bf5e5ab876b33044facf21cd3da909b4517dac5d48e27a5ecd43d272ff20c85.json similarity index 68% rename from apps/labrinth/.sqlx/query-e68e27fcb3e85233be06e7435aaeb6b27d8dbe2ddaf211ba37a026eab3bb6926.json rename to apps/labrinth/.sqlx/query-5bf5e5ab876b33044facf21cd3da909b4517dac5d48e27a5ecd43d272ff20c85.json index 5f6fbb751..ec7ef7ae1 100644 --- a/apps/labrinth/.sqlx/query-e68e27fcb3e85233be06e7435aaeb6b27d8dbe2ddaf211ba37a026eab3bb6926.json +++ b/apps/labrinth/.sqlx/query-5bf5e5ab876b33044facf21cd3da909b4517dac5d48e27a5ecd43d272ff20c85.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled')", + "query": "\n SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval, payment_platform, payment_platform_id, net, refunded\n FROM charges\n WHERE id = $1", "describe": { "columns": [ { @@ -57,6 +57,26 @@ "ordinal": 10, "name": "subscription_interval", "type_info": "Text" + }, + { + "ordinal": 11, + "name": "payment_platform", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "payment_platform_id", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "net", + "type_info": "Int8" + }, + { + "ordinal": 14, + "name": "refunded", + "type_info": "Int8" } ], "parameters": { @@ -75,8 +95,12 @@ true, false, true, - true + true, + false, + true, + false, + false ] }, - "hash": "e68e27fcb3e85233be06e7435aaeb6b27d8dbe2ddaf211ba37a026eab3bb6926" + "hash": "5bf5e5ab876b33044facf21cd3da909b4517dac5d48e27a5ecd43d272ff20c85" } diff --git a/apps/labrinth/.sqlx/query-68314da5dc020eb4692248c84b3a4c6c433502b0c72916b126137ff9a89f01db.json b/apps/labrinth/.sqlx/query-68314da5dc020eb4692248c84b3a4c6c433502b0c72916b126137ff9a89f01db.json new file mode 100644 index 000000000..009dff09f --- /dev/null +++ b/apps/labrinth/.sqlx/query-68314da5dc020eb4692248c84b3a4c6c433502b0c72916b126137ff9a89f01db.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, net, refunded)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)\n ON CONFLICT (id)\n DO UPDATE\n SET status = EXCLUDED.status,\n last_attempt = EXCLUDED.last_attempt,\n due = EXCLUDED.due,\n subscription_id = EXCLUDED.subscription_id,\n subscription_interval = EXCLUDED.subscription_interval,\n payment_platform = EXCLUDED.payment_platform,\n payment_platform_id = EXCLUDED.payment_platform_id,\n net = EXCLUDED.net,\n refunded = EXCLUDED.refunded\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int8", + "Int8", + "Text", + "Text", + "Varchar", + "Timestamptz", + "Timestamptz", + "Int8", + "Text", + "Text", + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "68314da5dc020eb4692248c84b3a4c6c433502b0c72916b126137ff9a89f01db" +} diff --git a/apps/labrinth/.sqlx/query-a87c913916adf9177f8f38369975d5fc644d989293ccb42c1e06ec54dc2571f8.json b/apps/labrinth/.sqlx/query-758550f2d8f968ea6b1f9197756dab77a095212557fd8410ee4fdfab670c007c.json similarity index 66% rename from apps/labrinth/.sqlx/query-a87c913916adf9177f8f38369975d5fc644d989293ccb42c1e06ec54dc2571f8.json rename to apps/labrinth/.sqlx/query-758550f2d8f968ea6b1f9197756dab77a095212557fd8410ee4fdfab670c007c.json index 66986fd92..26bec8dcc 100644 --- a/apps/labrinth/.sqlx/query-a87c913916adf9177f8f38369975d5fc644d989293ccb42c1e06ec54dc2571f8.json +++ b/apps/labrinth/.sqlx/query-758550f2d8f968ea6b1f9197756dab77a095212557fd8410ee4fdfab670c007c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval\n FROM charges\n WHERE (status = 'cancelled' AND due < $1) OR (status = 'failed' AND last_attempt < $1 - INTERVAL '2 days')", + "query": "\n SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval, payment_platform, payment_platform_id, net, refunded\n FROM charges\n WHERE (status = 'cancelled' AND due < $1) OR (status = 'failed' AND last_attempt < $1 - INTERVAL '2 days')", "describe": { "columns": [ { @@ -57,6 +57,26 @@ "ordinal": 10, "name": "subscription_interval", "type_info": "Text" + }, + { + "ordinal": 11, + "name": "payment_platform", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "payment_platform_id", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "net", + "type_info": "Int8" + }, + { + "ordinal": 14, + "name": "refunded", + "type_info": "Int8" } ], "parameters": { @@ -75,8 +95,12 @@ true, false, true, - true + true, + false, + true, + false, + false ] }, - "hash": "a87c913916adf9177f8f38369975d5fc644d989293ccb42c1e06ec54dc2571f8" + "hash": "758550f2d8f968ea6b1f9197756dab77a095212557fd8410ee4fdfab670c007c" } diff --git a/apps/labrinth/.sqlx/query-7c61fee015231f0a97c25d24f2c6be24821e39e330ab82344ad3b985d0d2aaea.json b/apps/labrinth/.sqlx/query-7c61fee015231f0a97c25d24f2c6be24821e39e330ab82344ad3b985d0d2aaea.json deleted file mode 100644 index adda594e1..000000000 --- a/apps/labrinth/.sqlx/query-7c61fee015231f0a97c25d24f2c6be24821e39e330ab82344ad3b985d0d2aaea.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT id FROM mods_gallery\n WHERE image_url = $1\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "7c61fee015231f0a97c25d24f2c6be24821e39e330ab82344ad3b985d0d2aaea" -} diff --git a/apps/labrinth/.sqlx/query-285cdd452fff85480dde02119d224a6e422e4041deb6f640ab5159d55ba2789c.json b/apps/labrinth/.sqlx/query-ab48060b1ef675d64f86bd0785474788b3403c3065b40ed0b20133c73a5575fb.json similarity index 66% rename from apps/labrinth/.sqlx/query-285cdd452fff85480dde02119d224a6e422e4041deb6f640ab5159d55ba2789c.json rename to apps/labrinth/.sqlx/query-ab48060b1ef675d64f86bd0785474788b3403c3065b40ed0b20133c73a5575fb.json index 72f8988c7..4db2b8913 100644 --- a/apps/labrinth/.sqlx/query-285cdd452fff85480dde02119d224a6e422e4041deb6f640ab5159d55ba2789c.json +++ b/apps/labrinth/.sqlx/query-ab48060b1ef675d64f86bd0785474788b3403c3065b40ed0b20133c73a5575fb.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval\n FROM charges\n WHERE (status = 'open' AND due < $1) OR (status = 'failed' AND last_attempt < $1 - INTERVAL '2 days')", + "query": "\n SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval, payment_platform, payment_platform_id, net, refunded\n FROM charges\n WHERE (status = 'open' AND due < $1) OR (status = 'failed' AND last_attempt < $1 - INTERVAL '2 days')", "describe": { "columns": [ { @@ -57,6 +57,26 @@ "ordinal": 10, "name": "subscription_interval", "type_info": "Text" + }, + { + "ordinal": 11, + "name": "payment_platform", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "payment_platform_id", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "net", + "type_info": "Int8" + }, + { + "ordinal": 14, + "name": "refunded", + "type_info": "Int8" } ], "parameters": { @@ -75,8 +95,12 @@ true, false, true, - true + true, + false, + true, + false, + false ] }, - "hash": "285cdd452fff85480dde02119d224a6e422e4041deb6f640ab5159d55ba2789c" + "hash": "ab48060b1ef675d64f86bd0785474788b3403c3065b40ed0b20133c73a5575fb" } diff --git a/apps/labrinth/.sqlx/query-457493bd11254ba1c9f81c47f15e8154053ae4e90e319d34a940fb73e33a69d4.json b/apps/labrinth/.sqlx/query-ce5440a69c04c52f66386e4e3f553ebba2f93052e3940c49701b8e7fa78636e6.json similarity index 68% rename from apps/labrinth/.sqlx/query-457493bd11254ba1c9f81c47f15e8154053ae4e90e319d34a940fb73e33a69d4.json rename to apps/labrinth/.sqlx/query-ce5440a69c04c52f66386e4e3f553ebba2f93052e3940c49701b8e7fa78636e6.json index 33d196a94..ebc80d0d7 100644 --- a/apps/labrinth/.sqlx/query-457493bd11254ba1c9f81c47f15e8154053ae4e90e319d34a940fb73e33a69d4.json +++ b/apps/labrinth/.sqlx/query-ce5440a69c04c52f66386e4e3f553ebba2f93052e3940c49701b8e7fa78636e6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval\n FROM charges\n WHERE user_id = $1 ORDER BY due DESC", + "query": "\n SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval, payment_platform, payment_platform_id, net, refunded\n FROM charges\n WHERE user_id = $1 ORDER BY due DESC", "describe": { "columns": [ { @@ -57,6 +57,26 @@ "ordinal": 10, "name": "subscription_interval", "type_info": "Text" + }, + { + "ordinal": 11, + "name": "payment_platform", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "payment_platform_id", + "type_info": "Text" + }, + { + "ordinal": 13, + "name": "net", + "type_info": "Int8" + }, + { + "ordinal": 14, + "name": "refunded", + "type_info": "Int8" } ], "parameters": { @@ -75,8 +95,12 @@ true, false, true, - true + true, + false, + true, + false, + false ] }, - "hash": "457493bd11254ba1c9f81c47f15e8154053ae4e90e319d34a940fb73e33a69d4" + "hash": "ce5440a69c04c52f66386e4e3f553ebba2f93052e3940c49701b8e7fa78636e6" } diff --git a/apps/labrinth/migrations/20241204190127_revenue_updates.sql b/apps/labrinth/migrations/20241204190127_revenue_updates.sql new file mode 100644 index 000000000..dada14a3a --- /dev/null +++ b/apps/labrinth/migrations/20241204190127_revenue_updates.sql @@ -0,0 +1,5 @@ +ALTER TABLE charges + ADD COLUMN payment_platform TEXT NOT NULL DEFAULT 'stripe', + ADD COLUMN payment_platform_id TEXT NULL, + ADD COLUMN net bigint not null DEFAULT 0, + ADD COLUMN refunded bigint not null DEFAULT 0; \ No newline at end of file diff --git a/apps/labrinth/src/database/models/charge_item.rs b/apps/labrinth/src/database/models/charge_item.rs index 80d75de58..3f6b93986 100644 --- a/apps/labrinth/src/database/models/charge_item.rs +++ b/apps/labrinth/src/database/models/charge_item.rs @@ -1,7 +1,9 @@ use crate::database::models::{ ChargeId, DatabaseError, ProductPriceId, UserId, UserSubscriptionId, }; -use crate::models::billing::{ChargeStatus, ChargeType, PriceDuration}; +use crate::models::billing::{ + ChargeStatus, ChargeType, PaymentPlatform, PriceDuration, +}; use chrono::{DateTime, Utc}; use std::convert::{TryFrom, TryInto}; @@ -9,7 +11,7 @@ pub struct ChargeItem { pub id: ChargeId, pub user_id: UserId, pub price_id: ProductPriceId, - pub amount: i64, + pub amount: u64, pub currency_code: String, pub status: ChargeStatus, pub due: DateTime, @@ -18,6 +20,13 @@ pub struct ChargeItem { pub type_: ChargeType, pub subscription_id: Option, pub subscription_interval: Option, + + pub payment_platform: PaymentPlatform, + pub payment_platform_id: Option, + + // Net is always in USD + pub net: u64, + pub refunded: u64, } struct ChargeResult { @@ -32,6 +41,10 @@ struct ChargeResult { charge_type: String, subscription_id: Option, subscription_interval: Option, + payment_platform: String, + payment_platform_id: Option, + net: i64, + refunded: i64, } impl TryFrom for ChargeItem { @@ -42,7 +55,7 @@ impl TryFrom for ChargeItem { id: ChargeId(r.id), user_id: UserId(r.user_id), price_id: ProductPriceId(r.price_id), - amount: r.amount, + amount: r.amount as u64, currency_code: r.currency_code, status: ChargeStatus::from_string(&r.status), due: r.due, @@ -52,6 +65,10 @@ impl TryFrom for ChargeItem { subscription_interval: r .subscription_interval .map(|x| PriceDuration::from_string(&x)), + payment_platform: PaymentPlatform::from_string(&r.payment_platform), + payment_platform_id: r.payment_platform_id, + net: r.net as u64, + refunded: r.refunded as u64, }) } } @@ -61,7 +78,7 @@ macro_rules! select_charges_with_predicate { sqlx::query_as!( ChargeResult, r#" - SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval + SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval, payment_platform, payment_platform_id, net, refunded FROM charges "# + $predicate, @@ -77,20 +94,24 @@ impl ChargeItem { ) -> Result { sqlx::query!( r#" - INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, net, refunded) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status, last_attempt = EXCLUDED.last_attempt, due = EXCLUDED.due, subscription_id = EXCLUDED.subscription_id, - subscription_interval = EXCLUDED.subscription_interval + subscription_interval = EXCLUDED.subscription_interval, + payment_platform = EXCLUDED.payment_platform, + payment_platform_id = EXCLUDED.payment_platform_id, + net = EXCLUDED.net, + refunded = EXCLUDED.refunded "#, self.id.0, self.user_id.0, self.price_id.0, - self.amount, + self.amount as i64, self.currency_code, self.type_.as_str(), self.status.as_str(), @@ -98,6 +119,10 @@ impl ChargeItem { self.last_attempt, self.subscription_id.map(|x| x.0), self.subscription_interval.map(|x| x.as_str()), + self.payment_platform.as_str(), + self.payment_platform_id.as_deref(), + self.net as i64, + self.refunded as i64, ) .execute(&mut **transaction) .await?; diff --git a/apps/labrinth/src/models/v3/billing.rs b/apps/labrinth/src/models/v3/billing.rs index afe879019..fca5b3e24 100644 --- a/apps/labrinth/src/models/v3/billing.rs +++ b/apps/labrinth/src/models/v3/billing.rs @@ -161,7 +161,7 @@ pub struct Charge { pub id: ChargeId, pub user_id: UserId, pub price_id: ProductPriceId, - pub amount: i64, + pub amount: u64, pub currency_code: String, pub status: ChargeStatus, pub due: DateTime, @@ -170,6 +170,7 @@ pub struct Charge { pub type_: ChargeType, pub subscription_id: Option, pub subscription_interval: Option, + pub platform: PaymentPlatform, } #[derive(Serialize, Deserialize)] @@ -208,6 +209,8 @@ pub enum ChargeStatus { Succeeded, Failed, Cancelled, + Refunded, + RefundProcessing, } impl ChargeStatus { @@ -229,6 +232,28 @@ impl ChargeStatus { ChargeStatus::Failed => "failed", ChargeStatus::Open => "open", ChargeStatus::Cancelled => "cancelled", + ChargeStatus::RefundProcessing => "refund_processing", + ChargeStatus::Refunded => "refunded", + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +pub enum PaymentPlatform { + Stripe, +} + +impl PaymentPlatform { + pub fn from_string(string: &str) -> PaymentPlatform { + match string { + "stripe" => PaymentPlatform::Stripe, + _ => PaymentPlatform::Stripe, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + PaymentPlatform::Stripe => "stripe", } } } diff --git a/apps/labrinth/src/routes/internal/billing.rs b/apps/labrinth/src/routes/internal/billing.rs index 188d30812..039819541 100644 --- a/apps/labrinth/src/routes/internal/billing.rs +++ b/apps/labrinth/src/routes/internal/billing.rs @@ -1,14 +1,14 @@ use crate::auth::{get_user_from_headers, send_email}; use crate::database::models::charge_item::ChargeItem; use crate::database::models::{ - generate_charge_id, generate_user_subscription_id, product_item, - user_subscription_item, + charge_item, generate_charge_id, generate_user_subscription_id, + product_item, user_subscription_item, }; use crate::database::redis::RedisPool; use crate::models::billing::{ - Charge, ChargeStatus, ChargeType, Price, PriceDuration, Product, - ProductMetadata, ProductPrice, SubscriptionMetadata, SubscriptionStatus, - UserSubscription, + Charge, ChargeStatus, ChargeType, PaymentPlatform, Price, PriceDuration, + Product, ProductMetadata, ProductPrice, SubscriptionMetadata, + SubscriptionStatus, UserSubscription, }; use crate::models::ids::base62_impl::{parse_base62, to_base62}; use crate::models::pats::Scopes; @@ -26,7 +26,7 @@ use sqlx::{PgPool, Postgres, Transaction}; use std::collections::{HashMap, HashSet}; use std::str::FromStr; use stripe::{ - CreateCustomer, CreatePaymentIntent, CreateSetupIntent, + CreateCustomer, CreatePaymentIntent, CreateRefund, CreateSetupIntent, CreateSetupIntentAutomaticPaymentMethods, CreateSetupIntentAutomaticPaymentMethodsAllowRedirects, Currency, CustomerId, CustomerInvoiceSettings, CustomerPaymentMethodRetrieval, @@ -111,6 +111,113 @@ pub async fn subscriptions( Ok(HttpResponse::Ok().json(subscriptions)) } +#[derive(Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ChargeRefund { + Full, + Partial { amount: u64 }, +} + +#[post("charge/{id}/refund")] +pub async fn refund_charge( + req: HttpRequest, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + info: web::Path<(crate::models::ids::ChargeId,)>, + body: web::Json, + stripe_client: web::Data, +) -> Result { + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Some(&[Scopes::SESSION_ACCESS]), + ) + .await? + .1; + + let (id,) = info.into_inner(); + + if !user.role.is_admin() { + return Err(ApiError::CustomAuthentication( + "You do not have permission to refund a subscription!".to_string(), + )); + } + + if let Some(mut charge) = + charge_item::ChargeItem::get(id.into(), &**pool).await? + { + let refundable = charge.amount - charge.refunded; + + let refund_amount = match body.0 { + ChargeRefund::Full => refundable, + ChargeRefund::Partial { amount } => amount, + }; + + if charge.status != ChargeStatus::Succeeded + && charge.status != ChargeStatus::Refunded + { + return Err(ApiError::InvalidInput( + "This charge cannot be refunded!".to_string(), + )); + } + + if (refundable - refund_amount) < 0 { + return Err(ApiError::InvalidInput( + "You cannot refund more than the amount of the charge!" + .to_string(), + )); + } + + let mut transaction = pool.begin().await?; + + charge.status = ChargeStatus::RefundProcessing; + charge.upsert(&mut transaction).await?; + + match charge.payment_platform { + PaymentPlatform::Stripe => { + if let Some(payment_platform_id) = charge + .payment_platform_id + .and_then(|x| stripe::PaymentIntentId::from_str(&x).ok()) + { + let mut metadata = HashMap::new(); + + metadata.insert( + "modrinth_charge_id".to_string(), + to_base62(charge.id.0 as u64), + ); + + stripe::Refund::create( + &stripe_client, + CreateRefund { + amount: Some(refund_amount as i64), + currency: Some( + Currency::from_str(&*charge.currency_code) + .unwrap_or(Currency::USD), + ), + metadata: Some(metadata), + payment_intent: Some(payment_platform_id), + + ..Default::default() + }, + ) + .await?; + } else { + return Err(ApiError::InvalidInput( + "Charge does not have attached payment id!".to_string(), + )); + } + } + } + + transaction.commit().await?; + } + + Ok(HttpResponse::NoContent().body("")) +} + #[derive(Deserialize)] pub struct SubscriptionEdit { pub interval: Option, @@ -195,7 +302,7 @@ pub async fn edit_subscription( if let Price::Recurring { intervals } = ¤t_price.prices { if let Some(price) = intervals.get(interval) { open_charge.subscription_interval = Some(*interval); - open_charge.amount = *price as i64; + open_charge.amount = *price as u64; } else { return Err(ApiError::InvalidInput( "Interval is not valid for this subscription!" @@ -276,7 +383,7 @@ pub async fn edit_subscription( id: charge_id, user_id: user.id.into(), price_id: product_price.id, - amount: proration as i64, + amount: proration as u64, currency_code: current_price.currency_code.clone(), status: ChargeStatus::Processing, due: Utc::now(), @@ -284,6 +391,10 @@ pub async fn edit_subscription( type_: ChargeType::Proration, subscription_id: Some(subscription.id), subscription_interval: Some(duration), + payment_platform: PaymentPlatform::Stripe, + payment_platform_id: None, + net: 0, + refunded: 0, }; let customer_id = get_or_create_customer( @@ -431,6 +542,7 @@ pub async fn charges( type_: x.type_, subscription_id: x.subscription_id.map(|x| x.into()), subscription_interval: x.subscription_interval, + platform: x.payment_platform, }) .collect::>(), )) @@ -979,7 +1091,7 @@ pub async fn initiate_payment( } ( - price as i64, + price as u64, price_item.currency_code, interval, price_item.id, @@ -1004,7 +1116,7 @@ pub async fn initiate_payment( if let Some(payment_intent_id) = &payment_request.existing_payment_intent { let mut update_payment_intent = stripe::UpdatePaymentIntent { - amount: Some(price), + amount: Some(price as i64), currency: Some(stripe_currency), customer: Some(customer), ..Default::default() @@ -1030,7 +1142,8 @@ pub async fn initiate_payment( "payment_method": payment_method, }))) } else { - let mut intent = CreatePaymentIntent::new(price, stripe_currency); + let mut intent = + CreatePaymentIntent::new(price as i64, stripe_currency); let mut metadata = HashMap::new(); metadata.insert("modrinth_user_id".to_string(), to_base62(user.id.0)); @@ -1318,7 +1431,7 @@ pub async fn stripe_webhook( id: charge_id, user_id, price_id, - amount: amount as i64, + amount: amount as u64, currency_code: price.currency_code.clone(), status: charge_status, due: Utc::now(), @@ -1332,6 +1445,11 @@ pub async fn stripe_webhook( subscription_interval: subscription .as_ref() .map(|x| x.interval), + payment_platform: PaymentPlatform::Stripe, + // TODO: fill these in (application fee) + payment_platform_id: None, + net: 0, + refunded: 0, }; if charge_status != ChargeStatus::Failed { @@ -1471,6 +1589,10 @@ pub async fn stripe_webhook( "storage_mb": storage, }, "source": source, + "payment_interval": metadata.charge_item.subscription_interval.map(|x| match x { + PriceDuration::Monthly => 1, + PriceDuration::Yearly => 3, + }) })) .send() .await? @@ -1514,7 +1636,7 @@ pub async fn stripe_webhook( if let Some(mut charge) = open_charge { charge.price_id = metadata.product_price_item.id; - charge.amount = new_price as i64; + charge.amount = new_price as u64; charge.upsert(&mut transaction).await?; } else if metadata.charge_item.status @@ -1526,7 +1648,7 @@ pub async fn stripe_webhook( id: charge_id, user_id: metadata.user_item.id, price_id: metadata.product_price_item.id, - amount: new_price as i64, + amount: new_price as u64, currency_code: metadata .product_price_item .currency_code, @@ -1546,6 +1668,13 @@ pub async fn stripe_webhook( subscription_interval: Some( subscription.interval, ), + payment_platform: PaymentPlatform::Stripe, + payment_platform_id: Some( + payment_intent.id.to_string(), + ), + // TODO: add application fee here + net: 0, + refunded: 0, } .upsert(&mut transaction) .await?; @@ -1596,7 +1725,7 @@ pub async fn stripe_webhook( if let Some(email) = metadata.user_item.email { let money = rusty_money::Money::from_minor( - metadata.charge_item.amount, + metadata.charge_item.amount as i64, rusty_money::iso::find( &metadata.charge_item.currency_code, ) diff --git a/apps/labrinth/src/routes/internal/statuses.rs b/apps/labrinth/src/routes/internal/statuses.rs index 3945447c2..a69f39a85 100644 --- a/apps/labrinth/src/routes/internal/statuses.rs +++ b/apps/labrinth/src/routes/internal/statuses.rs @@ -150,7 +150,12 @@ pub async fn ws_init( { let (status, _) = pair.value_mut(); - if status.profile_name.as_ref().map(|x| x.len() > 64).unwrap_or(false) { + if status + .profile_name + .as_ref() + .map(|x| x.len() > 64) + .unwrap_or(false) + { continue; } diff --git a/apps/labrinth/src/routes/v2/projects.rs b/apps/labrinth/src/routes/v2/projects.rs index 3ee33336b..7bc5eed60 100644 --- a/apps/labrinth/src/routes/v2/projects.rs +++ b/apps/labrinth/src/routes/v2/projects.rs @@ -861,7 +861,6 @@ pub struct GalleryEditQuery { pub async fn edit_gallery_item( req: HttpRequest, web::Query(item): web::Query, - info: web::Path<(String,)>, pool: web::Data, redis: web::Data, session_queue: web::Data, @@ -876,7 +875,6 @@ pub async fn edit_gallery_item( description: item.description, ordering: item.ordering, }), - info, pool, redis, session_queue, @@ -894,7 +892,6 @@ pub struct GalleryDeleteQuery { pub async fn delete_gallery_item( req: HttpRequest, web::Query(item): web::Query, - info: web::Path<(String,)>, pool: web::Data, redis: web::Data, file_host: web::Data>, @@ -904,7 +901,6 @@ pub async fn delete_gallery_item( v3::projects::delete_gallery_item( req, web::Query(v3::projects::GalleryDeleteQuery { url: item.url }), - info, pool, redis, file_host, diff --git a/apps/labrinth/src/routes/v3/projects.rs b/apps/labrinth/src/routes/v3/projects.rs index 5e7277ec3..0a47aea2c 100644 --- a/apps/labrinth/src/routes/v3/projects.rs +++ b/apps/labrinth/src/routes/v3/projects.rs @@ -1788,7 +1788,6 @@ pub struct GalleryEditQuery { pub async fn edit_gallery_item( req: HttpRequest, web::Query(item): web::Query, - info: web::Path<(String,)>, pool: web::Data, redis: web::Data, session_queue: web::Data, @@ -1802,19 +1801,38 @@ pub async fn edit_gallery_item( ) .await? .1; - let string = info.into_inner().0; item.validate().map_err(|err| { ApiError::Validation(validation_errors_to_string(err, None)) })?; - let project_item = db_models::Project::get(&string, &**pool, &redis) - .await? - .ok_or_else(|| { - ApiError::InvalidInput( - "The specified project does not exist!".to_string(), - ) - })?; + let result = sqlx::query!( + " + SELECT id, mod_id FROM mods_gallery + WHERE image_url = $1 + ", + item.url + ) + .fetch_optional(&**pool) + .await? + .ok_or_else(|| { + ApiError::InvalidInput(format!( + "Gallery item at URL {} is not part of the project's gallery.", + item.url + )) + })?; + + let project_item = db_models::Project::get_id( + database::models::ProjectId(result.mod_id), + &**pool, + &redis, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) + })?; if !user.role.is_mod() { let (team_member, organization_team_member) = @@ -1845,24 +1863,6 @@ pub async fn edit_gallery_item( )); } } - let mut transaction = pool.begin().await?; - - let id = sqlx::query!( - " - SELECT id FROM mods_gallery - WHERE image_url = $1 - ", - item.url - ) - .fetch_optional(&mut *transaction) - .await? - .ok_or_else(|| { - ApiError::InvalidInput(format!( - "Gallery item at URL {} is not part of the project's gallery.", - item.url - )) - })? - .id; let mut transaction = pool.begin().await?; @@ -1887,7 +1887,7 @@ pub async fn edit_gallery_item( SET featured = $2 WHERE id = $1 ", - id, + result.id, featured ) .execute(&mut *transaction) @@ -1900,7 +1900,7 @@ pub async fn edit_gallery_item( SET name = $2 WHERE id = $1 ", - id, + result.id, name ) .execute(&mut *transaction) @@ -1913,7 +1913,7 @@ pub async fn edit_gallery_item( SET description = $2 WHERE id = $1 ", - id, + result.id, description ) .execute(&mut *transaction) @@ -1926,7 +1926,7 @@ pub async fn edit_gallery_item( SET ordering = $2 WHERE id = $1 ", - id, + result.id, ordering ) .execute(&mut *transaction) @@ -1954,7 +1954,6 @@ pub struct GalleryDeleteQuery { pub async fn delete_gallery_item( req: HttpRequest, web::Query(item): web::Query, - info: web::Path<(String,)>, pool: web::Data, redis: web::Data, file_host: web::Data>, @@ -1969,15 +1968,34 @@ pub async fn delete_gallery_item( ) .await? .1; - let string = info.into_inner().0; - let project_item = db_models::Project::get(&string, &**pool, &redis) - .await? - .ok_or_else(|| { - ApiError::InvalidInput( - "The specified project does not exist!".to_string(), - ) - })?; + let item = sqlx::query!( + " + SELECT id, image_url, raw_image_url, mod_id FROM mods_gallery + WHERE image_url = $1 + ", + item.url + ) + .fetch_optional(&**pool) + .await? + .ok_or_else(|| { + ApiError::InvalidInput(format!( + "Gallery item at URL {} is not part of the project's gallery.", + item.url + )) + })?; + + let project_item = db_models::Project::get_id( + database::models::ProjectId(item.mod_id), + &**pool, + &redis, + ) + .await? + .ok_or_else(|| { + ApiError::InvalidInput( + "The specified project does not exist!".to_string(), + ) + })?; if !user.role.is_mod() { let (team_member, organization_team_member) = @@ -2009,23 +2027,6 @@ pub async fn delete_gallery_item( )); } } - let mut transaction = pool.begin().await?; - - let item = sqlx::query!( - " - SELECT id, image_url, raw_image_url FROM mods_gallery - WHERE image_url = $1 - ", - item.url - ) - .fetch_optional(&mut *transaction) - .await? - .ok_or_else(|| { - ApiError::InvalidInput(format!( - "Gallery item at URL {} is not part of the project's gallery.", - item.url - )) - })?; delete_old_images( Some(item.image_url),