From 2c1bcaafc1cd6c93d200041abdc91eb4700f216a Mon Sep 17 00:00:00 2001 From: Geometrically <18202329+Geometrically@users.noreply.github.com> Date: Mon, 7 Nov 2022 15:38:25 -0700 Subject: [PATCH] Use auto payments with paypal (#472) * Use auto payments with paypal * Remove sandbox key --- .env | 6 +- Cargo.lock | 85 +- Cargo.toml | 2 +- .../20221107171016_payouts-overhaul.sql | 24 + sqlx-data.json | 1342 +++++++++-------- src/database/models/team_item.rs | 20 +- src/database/models/user_item.rs | 70 +- src/main.rs | 9 + src/models/users.rs | 74 +- src/queue/mod.rs | 1 + src/queue/payouts.rs | 138 ++ src/routes/admin.rs | 183 ++- src/routes/auth.rs | 6 +- src/routes/mod.rs | 6 +- src/routes/users.rs | 251 ++- src/util/auth.rs | 9 +- src/util/mod.rs | 1 - src/util/payout_calc.rs | 22 - 18 files changed, 1424 insertions(+), 825 deletions(-) create mode 100644 migrations/20221107171016_payouts-overhaul.sql create mode 100644 src/queue/payouts.rs delete mode 100644 src/util/payout_calc.rs diff --git a/.env b/.env index 47c072aab..dcc76301a 100644 --- a/.env +++ b/.env @@ -48,4 +48,8 @@ ARIADNE_ADMIN_KEY=feedbeef ARIADNE_URL=https://staging-ariadne.modrinth.com/v1/ STRIPE_TOKEN=none -STRIPE_WEBHOOK_SECRET=none \ No newline at end of file +STRIPE_WEBHOOK_SECRET=none + +PAYPAL_API_URL=https://api-m.sandbox.paypal.com/v1/ +PAYPAL_CLIENT_ID=none +PAYPAL_CLIENT_SECRET=none diff --git a/Cargo.lock b/Cargo.lock index c6f7e4aef..fa1b7d5fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -425,6 +425,15 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -1420,6 +1429,15 @@ dependencies = [ "nom 5.1.2", ] +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.10.5" @@ -1475,7 +1493,7 @@ dependencies = [ "futures-timer", "hex", "hmac 0.11.0", - "itertools", + "itertools 0.10.5", "lazy_static", "log", "meilisearch-sdk", @@ -1564,6 +1582,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "local-channel" version = "0.1.3" @@ -1601,6 +1625,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "matches" version = "0.1.9" @@ -1795,6 +1828,12 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +[[package]] +name = "oncemutex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d11de466f4a3006fe8a5e7ec84e93b79c70cb992ae0aa0eb631ad2df8abfe2" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -1920,6 +1959,26 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +[[package]] +name = "phonenumber" +version = "0.3.1+8.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261a014e5f5e048bf2c6f1a72fa5e4c223009dc5f296a385b95fe19b464608f" +dependencies = [ + "bincode", + "either", + "fnv", + "itertools 0.9.0", + "lazy_static", + "nom 5.1.2", + "quick-xml", + "regex", + "regex-cache", + "serde", + "serde_derive", + "thiserror", +] + [[package]] name = "pin-project" version = "1.0.12" @@ -2011,6 +2070,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cc440ee4802a86e357165021e3e255a9143724da31db1e2ea540214c96a0f82" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.21" @@ -2081,6 +2149,18 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-cache" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7b62d69743b8b94f353b6b7c3deb4c5582828328bcb8d5fedf214373808793" +dependencies = [ + "lru-cache", + "oncemutex", + "regex", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.6.27" @@ -2516,7 +2596,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87e292b4291f154971a43c3774364e2cbcaec599d3f5bf6fa9d122885dbc38a" dependencies = [ - "itertools", + "itertools 0.10.5", "nom 7.1.1", "unicode_categories", ] @@ -2969,6 +3049,7 @@ checksum = "32ad5bf234c7d3ad1042e5252b7eddb2c4669ee23f32c7dd0e9b7705f07ef591" dependencies = [ "idna 0.2.3", "lazy_static", + "phonenumber", "regex", "serde", "serde_derive", diff --git a/Cargo.toml b/Cargo.toml index 292cfa6cd..82b0bc99c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,7 +56,7 @@ zip = { git = "https://github.com/zip-rs/zip", rev = "bb230ef56adc13436d1fcdfaa4 itertools = "0.10.5" -validator = { version = "0.16.0", features = ["derive"] } +validator = { version = "0.16.0", features = ["derive", "phone"] } regex = "1.6.0" censor = "0.2.0" diff --git a/migrations/20221107171016_payouts-overhaul.sql b/migrations/20221107171016_payouts-overhaul.sql new file mode 100644 index 000000000..b79424623 --- /dev/null +++ b/migrations/20221107171016_payouts-overhaul.sql @@ -0,0 +1,24 @@ +ALTER TABLE users DROP COLUMN paypal_email; +ALTER TABLE payouts_values DROP COLUMN claimed; + +ALTER TABLE users ADD COLUMN payout_wallet varchar(128) NULL; +ALTER TABLE users ADD COLUMN payout_wallet_type varchar(128) NULL; +ALTER TABLE users ADD COLUMN payout_address varchar(128) NULL; +ALTER TABLE users ADD COLUMN balance numeric(96, 48) NOT NULL DEFAULT 0; + +UPDATE users +SET balance = COALESCE((SELECT SUM(T2.amount) FROM payouts_values T2 WHERE T2.user_id = users.id), 0, 0) +WHERE id > 1; + +CREATE TABLE historical_payouts ( + id bigserial PRIMARY KEY, + user_id bigint REFERENCES users NOT NULL, + amount numeric(96, 48) NOT NULL, + created timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL, + status varchar(128) NOT NULL +); + +DELETE FROM payouts_values WHERE amount = 0; + +CREATE INDEX historical_payouts_user_id + ON historical_payouts (user_id); \ No newline at end of file diff --git a/sqlx-data.json b/sqlx-data.json index d6dd3d5a0..7f1e597dd 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -144,6 +144,18 @@ }, "query": "\n UPDATE versions\n SET downloads = $1\n WHERE (id = $2)\n " }, + "0794b913ad194908048fa8f303f736f3d437a1699b975691f9d50e8ffc3f5f78": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n DELETE FROM historical_payouts\n WHERE user_id = $1\n " + }, "07ebc9dc82cd012cd4f5880b1eb3d82602c195a3e3ddd557103ee037aa6dad1c": { "describe": { "columns": [], @@ -417,6 +429,26 @@ }, "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.body body, m.body_url body_url, m.published published,\n m.updated updated, m.approved approved, m.status status,\n m.issues_url issues_url, m.source_url source_url, m.wiki_url wiki_url, m.discord_url discord_url, m.license_url license_url,\n m.team_id team_id, m.client_side client_side, m.server_side server_side, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, l.name license_name, pt.name project_type_name,\n ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT v.id || ' |||| ' || v.date_published) filter (where v.id is not null) versions,\n ARRAY_AGG(DISTINCT mg.image_url || ' |||| ' || mg.featured || ' |||| ' || mg.created || ' |||| ' || COALESCE(mg.title, ' ') || ' |||| ' || COALESCE(mg.description, ' ')) filter (where mg.image_url is not null) gallery,\n ARRAY_AGG(DISTINCT md.joining_platform_id || ' |||| ' || dp.short || ' |||| ' || dp.name || ' |||| ' || md.url) filter (where md.joining_platform_id is not null) donations\n FROM mods m\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n LEFT JOIN mods_donations md ON md.joining_mod_id = m.id\n LEFT JOIN donation_platforms dp ON md.joining_platform_id = dp.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n LEFT JOIN versions v ON v.mod_id = m.id\n LEFT JOIN mods_gallery mg ON mg.mod_id = m.id\n WHERE m.id = ANY($1)\n GROUP BY pt.id, s.id, cs.id, ss.id, l.id, m.id;\n " }, + "0ba5a9f4d1381ed37a67b7dc90edf7e3ec86cae6c2860e5db1e53144d4654e58": { + "describe": { + "columns": [ + { + "name": "amount", + "ordinal": 0, + "type_info": "Numeric" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT SUM(pv.amount) amount\n FROM payouts_values pv\n WHERE pv.user_id = $1\n " + }, "0ca11a32b2860e4f5c3d20892a5be3cb419e084f42ba0f98e09b9995027fcc4e": { "describe": { "columns": [ @@ -689,6 +721,21 @@ }, "query": "\n INSERT INTO categories (category, project_type, icon, header)\n VALUES ($1, $2, $3, $4)\n RETURNING id\n " }, + "196c8ac2228e199f23eaf980f7ea15b37f76e66bb81da1115a754aad0be756e4": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Numeric", + "Timestamptz" + ] + } + }, + "query": "\n INSERT INTO payouts_values (user_id, mod_id, amount, created)\n VALUES ($1, $2, $3, $4)\n " + }, "19dc22c4d6d14222f8e8bace74c2961761c53b7375460ade15af921754d5d7da": { "describe": { "columns": [], @@ -999,21 +1046,6 @@ }, "query": "\n SELECT m.id id, tm.user_id user_id, tm.payouts_split payouts_split\n FROM mods m\n INNER JOIN team_members tm on m.team_id = tm.team_id AND tm.accepted = TRUE\n WHERE m.id = ANY($1)\n " }, - "2576a47d17794598f9318d1b2d2892006c8af165a188a8695c0f0b4837082eb9": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Numeric", - "Timestamptz" - ] - } - }, - "query": "\n INSERT INTO payouts_values (user_id, mod_id, amount, created)\n VALUES ($1, $2, $3, $4)\n " - }, "27a35fca63dfc3801f95958604f0ac27afd81800e2dc981382d6f923c4415d32": { "describe": { "columns": [], @@ -1195,86 +1227,6 @@ }, "query": "DELETE FROM banned_users WHERE github_id = $1;" }, - "316dd63b0ffdd3e9fc30dcbe28a4da7c3c76f4f569e9cc6204dd884e17e0fe5c": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "github_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "name", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "email", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "avatar_url", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "username", - "ordinal": 5, - "type_info": "Varchar" - }, - { - "name": "bio", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "created", - "ordinal": 7, - "type_info": "Timestamptz" - }, - { - "name": "role", - "ordinal": 8, - "type_info": "Varchar" - }, - { - "name": "badges", - "ordinal": 9, - "type_info": "Int8" - }, - { - "name": "paypal_email", - "ordinal": 10, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - true, - true, - true, - true, - false, - true, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "\n SELECT u.id, u.github_id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges, u.paypal_email\n FROM users u\n WHERE LOWER(u.username) = LOWER($1)\n " - }, "33a965c7dc615d3b701c05299889357db8dd36d378850625d2602ba471af4885": { "describe": { "columns": [], @@ -1630,44 +1582,6 @@ }, "query": "\n UPDATE mods\n SET title = $1\n WHERE (id = $2)\n " }, - "3eb4717d7c7e46dca288e99a8afa9c6b67ac2baa5132d9371ca557fdcdf3a7a3": { - "describe": { - "columns": [ - { - "name": "mod_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "created", - "ordinal": 1, - "type_info": "Timestamptz" - }, - { - "name": "claimed", - "ordinal": 2, - "type_info": "Bool" - }, - { - "name": "amount", - "ordinal": 3, - "type_info": "Numeric" - } - ], - "nullable": [ - true, - false, - false, - false - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT pv.mod_id, pv.created, pv.claimed, pv.amount\n FROM payouts_values pv\n WHERE pv.user_id = $1\n ORDER BY pv.created DESC\n " - }, "3f2f05653552ce8c1be95ce0a922ab41f52f40f8ff6c91c6621481102c8f35e3": { "describe": { "columns": [], @@ -1681,122 +1595,6 @@ }, "query": "\n INSERT INTO game_versions_versions (game_version_id, joining_version_id)\n VALUES ($1, $2)\n " }, - "3f33b1a2d6003c4654a1de1fe9e4c784490768148ee5dc9303ccd7aa528e3e9c": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "team_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "member_role", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "permissions", - "ordinal": 3, - "type_info": "Int8" - }, - { - "name": "accepted", - "ordinal": 4, - "type_info": "Bool" - }, - { - "name": "payouts_split", - "ordinal": 5, - "type_info": "Numeric" - }, - { - "name": "user_id", - "ordinal": 6, - "type_info": "Int8" - }, - { - "name": "github_id", - "ordinal": 7, - "type_info": "Int8" - }, - { - "name": "user_name", - "ordinal": 8, - "type_info": "Varchar" - }, - { - "name": "email", - "ordinal": 9, - "type_info": "Varchar" - }, - { - "name": "avatar_url", - "ordinal": 10, - "type_info": "Varchar" - }, - { - "name": "username", - "ordinal": 11, - "type_info": "Varchar" - }, - { - "name": "bio", - "ordinal": 12, - "type_info": "Varchar" - }, - { - "name": "created", - "ordinal": 13, - "type_info": "Timestamptz" - }, - { - "name": "user_role", - "ordinal": 14, - "type_info": "Varchar" - }, - { - "name": "badges", - "ordinal": 15, - "type_info": "Int8" - }, - { - "name": "paypal_email", - "ordinal": 16, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - true, - false, - true, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Int8Array" - ] - } - }, - "query": "\n SELECT tm.id id, tm.team_id team_id, tm.role member_role, tm.permissions permissions, tm.accepted accepted, tm.payouts_split payouts_split,\n u.id user_id, u.github_id github_id, u.name user_name, u.email email,\n u.avatar_url avatar_url, u.username username, u.bio bio,\n u.created created, u.role user_role, u.badges badges, u.paypal_email paypal_email\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = ANY($1)\n ORDER BY tm.team_id\n " - }, "40f7c5bec98fe3503d6bd6db2eae5a4edb8d5d6efda9b9dc124f344ae5c60e08": { "describe": { "columns": [], @@ -1821,116 +1619,6 @@ }, "query": "\n UPDATE users\n SET stripe_customer_id = NULL, midas_expires = NULL, is_overdue = NULL\n WHERE (stripe_customer_id = $1)\n " }, - "42241f8e3e482e5f9f31fb7ed81dc78a01aa435879e88cfb9eca8f458c26c4bc": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "member_role", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "permissions", - "ordinal": 2, - "type_info": "Int8" - }, - { - "name": "accepted", - "ordinal": 3, - "type_info": "Bool" - }, - { - "name": "payouts_split", - "ordinal": 4, - "type_info": "Numeric" - }, - { - "name": "user_id", - "ordinal": 5, - "type_info": "Int8" - }, - { - "name": "github_id", - "ordinal": 6, - "type_info": "Int8" - }, - { - "name": "user_name", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "email", - "ordinal": 8, - "type_info": "Varchar" - }, - { - "name": "avatar_url", - "ordinal": 9, - "type_info": "Varchar" - }, - { - "name": "username", - "ordinal": 10, - "type_info": "Varchar" - }, - { - "name": "bio", - "ordinal": 11, - "type_info": "Varchar" - }, - { - "name": "created", - "ordinal": 12, - "type_info": "Timestamptz" - }, - { - "name": "user_role", - "ordinal": 13, - "type_info": "Varchar" - }, - { - "name": "badges", - "ordinal": 14, - "type_info": "Int8" - }, - { - "name": "paypal_email", - "ordinal": 15, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - false, - false, - false, - false, - false, - true, - true, - true, - true, - false, - true, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT tm.id id, tm.role member_role, tm.permissions permissions, tm.accepted accepted, tm.payouts_split payouts_split,\n u.id user_id, u.github_id github_id, u.name user_name, u.email email,\n u.avatar_url avatar_url, u.username username, u.bio bio,\n u.created created, u.role user_role, u.badges badges, u.paypal_email paypal_email\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = $1\n " - }, "4298552497a48adb9ace61c8dcf989c4d35866866b61c0cc4d45909b1d31c660": { "describe": { "columns": [ @@ -2022,6 +1710,20 @@ }, "query": "\n INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id)\n VALUES ($1, $2)\n " }, + "4778d2f5994fda2f978fa53e0840c1a9a2582ef0434a5ff7f21706f1dc4edcf4": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Numeric", + "Varchar" + ] + } + }, + "query": "\n INSERT INTO historical_payouts (user_id, amount, status)\n VALUES ($1, $2, $3)\n " + }, "48294a4e0c594e80fff8d14a705aa7282f55e47cf3772e77f1d4bf4849008b60": { "describe": { "columns": [ @@ -2272,7 +1974,7 @@ }, "query": "\n DELETE FROM game_versions_versions WHERE joining_version_id = $1\n " }, - "51a1ae25a419ecd73938206786dae34e0e53ef4ef2cb0112f185412ce7e52226": { + "50fc72532d4b61d117b6245b5e315f3d88e9fe3a67f4522d281270911177f3ed": { "describe": { "columns": [ { @@ -2281,48 +1983,68 @@ "type_info": "Int8" }, { - "name": "name", + "name": "github_id", "ordinal": 1, - "type_info": "Varchar" + "type_info": "Int8" }, { - "name": "email", + "name": "name", "ordinal": 2, "type_info": "Varchar" }, { - "name": "avatar_url", + "name": "email", "ordinal": 3, "type_info": "Varchar" }, { - "name": "username", + "name": "avatar_url", "ordinal": 4, "type_info": "Varchar" }, { - "name": "bio", + "name": "username", "ordinal": 5, "type_info": "Varchar" }, { - "name": "created", + "name": "bio", "ordinal": 6, + "type_info": "Varchar" + }, + { + "name": "created", + "ordinal": 7, "type_info": "Timestamptz" }, { "name": "role", - "ordinal": 7, + "ordinal": 8, "type_info": "Varchar" }, { "name": "badges", - "ordinal": 8, + "ordinal": 9, "type_info": "Int8" }, { - "name": "paypal_email", - "ordinal": 9, + "name": "balance", + "ordinal": 10, + "type_info": "Numeric" + }, + { + "name": "payout_wallet", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "payout_wallet_type", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "payout_address", + "ordinal": 13, "type_info": "Varchar" } ], @@ -2331,20 +2053,24 @@ true, true, true, + true, false, true, false, false, false, + false, + true, + true, true ], "parameters": { "Left": [ - "Int8" + "Text" ] } }, - "query": "\n SELECT u.id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges, u.paypal_email\n FROM users u\n WHERE u.github_id = $1\n " + "query": "\n SELECT u.id, u.github_id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges,\n u.balance, u.payout_wallet, u.payout_wallet_type,\n u.payout_address\n FROM users u\n WHERE LOWER(u.username) = LOWER($1)\n " }, "5295fba2053675c8414c0b37a59943535b9a438a642ea1c68045e987f05ade13": { "describe": { @@ -2995,6 +2721,38 @@ }, "query": "\n INSERT INTO loaders_versions (loader_id, version_id)\n VALUES ($1, $2)\n " }, + "6d10ec782e422e868681827a6eb999edc6bf4fe8fa2b94d1f8970db2578c6db4": { + "describe": { + "columns": [ + { + "name": "created", + "ordinal": 0, + "type_info": "Timestamptz" + }, + { + "name": "amount", + "ordinal": 1, + "type_info": "Numeric" + }, + { + "name": "status", + "ordinal": 2, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + false, + false + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT hp.created, hp.amount, hp.status\n FROM historical_payouts hp\n WHERE hp.user_id = $1\n ORDER BY hp.created DESC\n " + }, "6d883ea05aead20f571a0f63bfd63f1d432717ec7a0fb9ab29e01fcb061b3afc": { "describe": { "columns": [], @@ -3050,19 +2808,6 @@ }, "query": "\n UPDATE dependencies\n SET dependency_id = $2\n WHERE id = ANY($1::bigint[])\n " }, - "71e9ffb27dbfad768d6109e8c77441b8c75c0278904305b7bbdf75548be9d577": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Timestamptz" - ] - } - }, - "query": "\n UPDATE payouts_values\n SET claimed = TRUE\n WHERE (claimed = FALSE AND user_id = $1 AND created <= $2)\n " - }, "72ad6f4be40d7620a0ec557e3806da41ce95335aeaa910fe35aca2ec7c3f09b6": { "describe": { "columns": [ @@ -3370,6 +3115,39 @@ }, "query": "\n INSERT INTO mods (\n id, team_id, title, description, body,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status, discord_url,\n client_side, server_side, license_url, license,\n slug, project_type\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13,\n $14, $15, $16, $17,\n LOWER($18), $19\n )\n " }, + "7ab21e7613dd88e97cf602e76bff62170c13ceef8104a4ce4cb2d101f8ce4f48": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Numeric", + "Int8" + ] + } + }, + "query": "\n UPDATE users\n SET balance = balance + $1\n WHERE id = $2\n " + }, + "7c0cdacf0898155c94008a96a0b918550df4475b9e3362a926d4d00e001880c1": { + "describe": { + "columns": [ + { + "name": "amount", + "ordinal": 0, + "type_info": "Numeric" + } + ], + "nullable": [ + null + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT SUM(pv.amount) amount\n FROM payouts_values pv\n WHERE pv.user_id = $1 AND created > NOW() - '1 month'::interval\n " + }, "7c61fee015231f0a97c25d24f2c6be24821e39e330ab82344ad3b985d0d2aaea": { "describe": { "columns": [ @@ -3450,6 +3228,140 @@ }, "query": "\n INSERT INTO notifications_actions (\n notification_id, title, action_route, action_route_method\n )\n VALUES (\n $1, $2, $3, $4\n )\n " }, + "8187df6b7a47085ec9f844737755c75b162d154a6c38ee2f88c68ebc673baaab": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "team_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "member_role", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "permissions", + "ordinal": 3, + "type_info": "Int8" + }, + { + "name": "accepted", + "ordinal": 4, + "type_info": "Bool" + }, + { + "name": "payouts_split", + "ordinal": 5, + "type_info": "Numeric" + }, + { + "name": "user_id", + "ordinal": 6, + "type_info": "Int8" + }, + { + "name": "github_id", + "ordinal": 7, + "type_info": "Int8" + }, + { + "name": "user_name", + "ordinal": 8, + "type_info": "Varchar" + }, + { + "name": "email", + "ordinal": 9, + "type_info": "Varchar" + }, + { + "name": "avatar_url", + "ordinal": 10, + "type_info": "Varchar" + }, + { + "name": "username", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "bio", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "created", + "ordinal": 13, + "type_info": "Timestamptz" + }, + { + "name": "user_role", + "ordinal": 14, + "type_info": "Varchar" + }, + { + "name": "badges", + "ordinal": 15, + "type_info": "Int8" + }, + { + "name": "balance", + "ordinal": 16, + "type_info": "Numeric" + }, + { + "name": "payout_wallet", + "ordinal": 17, + "type_info": "Varchar" + }, + { + "name": "payout_wallet_type", + "ordinal": 18, + "type_info": "Varchar" + }, + { + "name": "payout_address", + "ordinal": 19, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + false, + true, + false, + false, + false, + false, + true, + true, + true + ], + "parameters": { + "Left": [ + "Int8Array" + ] + } + }, + "query": "\n SELECT tm.id id, tm.team_id team_id, tm.role member_role, tm.permissions permissions, tm.accepted accepted, tm.payouts_split payouts_split,\n u.id user_id, u.github_id github_id, u.name user_name, u.email email,\n u.avatar_url avatar_url, u.username username, u.bio bio,\n u.created created, u.role user_role, u.badges badges, u.balance balance,\n u.payout_wallet payout_wallet, u.payout_wallet_type payout_wallet_type,\n u.payout_address payout_address\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = ANY($1)\n ORDER BY tm.team_id\n " + }, "82515e4e7e88f1193c956f032caabc70f535f925e212de30f974afd3ec126092": { "describe": { "columns": [ @@ -3504,6 +3416,98 @@ }, "query": "\n DELETE FROM project_types\n WHERE name = $1\n " }, + "886cc346f5ecc958018f7cab7dc3db9f8766fcdc7b16d686504ddcb6c5dde0b0": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "email", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "avatar_url", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "username", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "bio", + "ordinal": 5, + "type_info": "Varchar" + }, + { + "name": "created", + "ordinal": 6, + "type_info": "Timestamptz" + }, + { + "name": "role", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "badges", + "ordinal": 8, + "type_info": "Int8" + }, + { + "name": "balance", + "ordinal": 9, + "type_info": "Numeric" + }, + { + "name": "payout_wallet", + "ordinal": 10, + "type_info": "Varchar" + }, + { + "name": "payout_wallet_type", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "payout_address", + "ordinal": 12, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + true, + true, + true, + false, + true, + false, + false, + false, + false, + true, + true, + true + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT u.id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges,\n u.balance, u.payout_wallet, u.payout_wallet_type,\n u.payout_address\n FROM users u\n WHERE u.github_id = $1\n " + }, "8a7b2bc070e5e8308e2853ff125bc98f40b22c1d0deeb013dd90ce5768bd0ce8": { "describe": { "columns": [], @@ -3978,18 +3982,6 @@ }, "query": "\n SELECT u.stripe_customer_id\n FROM users u\n WHERE u.id = $1\n " }, - "9e0620900b225d9ffa4015382843ce4cc02f9ecda6555c81089b43952510e7e7": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Timestamptz" - ] - } - }, - "query": "\n DELETE FROM payouts_values\n WHERE created = $1\n " - }, "a0148ff25855202e7bb220b6a2bc9220a95e309fb0dae41d9a05afa86e6b33af": { "describe": { "columns": [], @@ -4014,80 +4006,6 @@ }, "query": "\n UPDATE mods\n SET approved = NOW()\n WHERE id = $1 AND approved IS NULL\n " }, - "a121d8e7e57dd94259e420e370513f738c72aacc7e9f342050cdd70cb3cb478e": { - "describe": { - "columns": [ - { - "name": "github_id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "name", - "ordinal": 1, - "type_info": "Varchar" - }, - { - "name": "email", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "avatar_url", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "username", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "bio", - "ordinal": 5, - "type_info": "Varchar" - }, - { - "name": "created", - "ordinal": 6, - "type_info": "Timestamptz" - }, - { - "name": "role", - "ordinal": 7, - "type_info": "Varchar" - }, - { - "name": "badges", - "ordinal": 8, - "type_info": "Int8" - }, - { - "name": "paypal_email", - "ordinal": 9, - "type_info": "Varchar" - } - ], - "nullable": [ - true, - true, - true, - true, - false, - true, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Int8" - ] - } - }, - "query": "\n SELECT u.github_id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges, u.paypal_email\n FROM users u\n WHERE u.id = $1\n " - }, "a2c3f1dc8939a0df9cb62e7e751847b7681b96b4016389cf5f39ebd1deff6e5a": { "describe": { "columns": [], @@ -4222,6 +4140,104 @@ }, "query": "\n DELETE FROM states\n WHERE expires < CURRENT_DATE\n " }, + "a91e7409e72211acf36cdcc4ee3395ef350acbf7be401e190dfbabdf60ebe155": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "github_id", + "ordinal": 1, + "type_info": "Int8" + }, + { + "name": "name", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "email", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "avatar_url", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "username", + "ordinal": 5, + "type_info": "Varchar" + }, + { + "name": "bio", + "ordinal": 6, + "type_info": "Varchar" + }, + { + "name": "created", + "ordinal": 7, + "type_info": "Timestamptz" + }, + { + "name": "role", + "ordinal": 8, + "type_info": "Varchar" + }, + { + "name": "badges", + "ordinal": 9, + "type_info": "Int8" + }, + { + "name": "balance", + "ordinal": 10, + "type_info": "Numeric" + }, + { + "name": "payout_wallet", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "payout_wallet_type", + "ordinal": 12, + "type_info": "Varchar" + }, + { + "name": "payout_address", + "ordinal": 13, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + true, + true, + true, + true, + false, + true, + false, + false, + false, + false, + true, + true, + true + ], + "parameters": { + "Left": [ + "Int8Array" + ] + } + }, + "query": "\n SELECT u.id, u.github_id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges,\n u.balance, u.payout_wallet, u.payout_wallet_type,\n u.payout_address\n FROM users u\n WHERE u.id = ANY($1)\n " + }, "a91fabe9e620bd700362c68631628725419183025c9699f4bd31c22b813b2824": { "describe": { "columns": [ @@ -4467,6 +4483,19 @@ }, "query": "\n DELETE FROM teams\n WHERE id = $1\n " }, + "b1e77dbaf4b190ab361f4fa203c442e5905cef6c1a135011a59ebd6e2dc0a92a": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Numeric", + "Int8" + ] + } + }, + "query": "\n UPDATE users\n SET balance = balance - $1\n WHERE id = $2\n " + }, "b41ba860c9d5402ba78297800b9df632a45718f5680a4e96d05372e59466ed7d": { "describe": { "columns": [ @@ -4621,6 +4650,98 @@ }, "query": "\n SELECT m.id id, m.project_type project_type, m.title title, m.description description, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.published published, m.approved approved, m.updated updated,\n m.team_id team_id, m.license license, m.slug slug,\n s.status status_name, cs.name client_side_type, ss.name server_side_type, l.short short, pt.name project_type_name, u.username username,\n ARRAY_AGG(DISTINCT c.category || ' |||| ' || mc.is_additional) filter (where c.category is not null) categories,\n ARRAY_AGG(DISTINCT lo.loader) filter (where lo.loader is not null) loaders,\n ARRAY_AGG(DISTINCT gv.version) filter (where gv.version is not null) versions,\n ARRAY_AGG(DISTINCT mg.image_url) filter (where mg.image_url is not null) gallery\n FROM mods m\n LEFT OUTER JOIN mods_categories mc ON joining_mod_id = m.id\n LEFT OUTER JOIN categories c ON mc.joining_category_id = c.id\n LEFT OUTER JOIN versions v ON v.mod_id = m.id\n LEFT OUTER JOIN game_versions_versions gvv ON gvv.joining_version_id = v.id\n LEFT OUTER JOIN game_versions gv ON gvv.game_version_id = gv.id\n LEFT OUTER JOIN loaders_versions lv ON lv.version_id = v.id\n LEFT OUTER JOIN loaders lo ON lo.id = lv.loader_id\n LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id\n INNER JOIN statuses s ON s.id = m.status\n INNER JOIN project_types pt ON pt.id = m.project_type\n INNER JOIN side_types cs ON m.client_side = cs.id\n INNER JOIN side_types ss ON m.server_side = ss.id\n INNER JOIN licenses l ON m.license = l.id\n INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE\n INNER JOIN users u ON tm.user_id = u.id\n WHERE s.status = $1 OR s.status = $2\n GROUP BY m.id, s.id, cs.id, ss.id, l.id, pt.id, u.id;\n " }, + "b4e67474dcef7d8357c3336b154425d1fff9d70757f8393e285883b739f5fadf": { + "describe": { + "columns": [ + { + "name": "github_id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "name", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "email", + "ordinal": 2, + "type_info": "Varchar" + }, + { + "name": "avatar_url", + "ordinal": 3, + "type_info": "Varchar" + }, + { + "name": "username", + "ordinal": 4, + "type_info": "Varchar" + }, + { + "name": "bio", + "ordinal": 5, + "type_info": "Varchar" + }, + { + "name": "created", + "ordinal": 6, + "type_info": "Timestamptz" + }, + { + "name": "role", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "badges", + "ordinal": 8, + "type_info": "Int8" + }, + { + "name": "balance", + "ordinal": 9, + "type_info": "Numeric" + }, + { + "name": "payout_wallet", + "ordinal": 10, + "type_info": "Varchar" + }, + { + "name": "payout_wallet_type", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "payout_address", + "ordinal": 12, + "type_info": "Varchar" + } + ], + "nullable": [ + true, + true, + true, + true, + false, + true, + false, + false, + false, + false, + true, + true, + true + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT u.github_id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges,\n u.balance, u.payout_wallet, u.payout_wallet_type,\n u.payout_address\n FROM users u\n WHERE u.id = $1\n " + }, "b69a6f42965b3e7103fcbf46e39528466926789ff31e9ed2591bb175527ec169": { "describe": { "columns": [], @@ -4719,19 +4840,6 @@ }, "query": "\n DELETE FROM notifications_actions\n WHERE notification_id = ANY($1)\n " }, - "bba84e2856e9566f90130d99dc2a9338ac4efdb638c9830a9721c022f1fef5b7": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Int8" - ] - } - }, - "query": "\n UPDATE users\n SET paypal_email = $1\n WHERE (id = $2)\n " - }, "bbfb47ae2c972734785df6b7c3e62077dc544ef4ccf8bb89e9c22c2f50a933c1": { "describe": { "columns": [], @@ -4924,6 +5032,21 @@ }, "query": "\n UPDATE mods\n SET license_url = $1\n WHERE (id = $2)\n " }, + "c11a0f7ca3959ede1655c4f244cfe4461701218d26e28152306bca5c46f1abd5": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Varchar", + "Varchar", + "Varchar", + "Int8" + ] + } + }, + "query": "\n UPDATE users\n SET payout_wallet = $1, payout_wallet_type = $2, payout_address = $3\n WHERE (id = $4)\n " + }, "c15534b7259e2b138c6f041bf2a9f4c77bea060a9bce6f2a829a2d7594dddd3a": { "describe": { "columns": [ @@ -5128,21 +5251,6 @@ }, "query": "\n INSERT INTO mod_follows (follower_id, mod_id)\n VALUES ($1, $2)\n " }, - "c5bfaf443d56825fb1f53ace39c5fecfa14957b66889197754fb3a4eef9a7538": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Numeric", - "Timestamptz" - ] - } - }, - "query": "\n INSERT INTO payouts_values (user_id, mod_id, amount, created)\n VALUES ($1, $2, $3, $4)\n " - }, "c5d44333c62223bd3e68185d1fb3f95152fafec593da8d06c9b2b665218a02be": { "describe": { "columns": [], @@ -5301,32 +5409,6 @@ }, "query": "\n DELETE FROM reports\n WHERE mod_id = $1\n " }, - "cca6c762835178aec0b5bbf677f106a5463751083667c9376f7c17a2436b48c5": { - "describe": { - "columns": [ - { - "name": "paypal_email", - "ordinal": 0, - "type_info": "Varchar" - }, - { - "name": "amount", - "ordinal": 1, - "type_info": "Numeric" - } - ], - "nullable": [ - true, - null - ], - "parameters": { - "Left": [ - "Timestamptz" - ] - } - }, - "query": "\n SELECT u.paypal_email, SUM(pv.amount) amount\n FROM payouts_values pv\n INNER JOIN users u ON pv.user_id = u.id AND u.paypal_email IS NOT NULL\n WHERE pv.created <= $1 AND pv.claimed = FALSE\n GROUP BY u.paypal_email\n " - }, "ccd913bb2f3006ffe881ce2fc4ef1e721d18fe2eed6ac62627046c955129610c": { "describe": { "columns": [ @@ -5729,6 +5811,134 @@ }, "query": "\n SELECT id FROM donation_platforms\n WHERE short = $1\n " }, + "d83c10db0ab21343ab17acad30546ecebeae2347b12c4604532eccf66dd99b7a": { + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int8" + }, + { + "name": "member_role", + "ordinal": 1, + "type_info": "Varchar" + }, + { + "name": "permissions", + "ordinal": 2, + "type_info": "Int8" + }, + { + "name": "accepted", + "ordinal": 3, + "type_info": "Bool" + }, + { + "name": "payouts_split", + "ordinal": 4, + "type_info": "Numeric" + }, + { + "name": "user_id", + "ordinal": 5, + "type_info": "Int8" + }, + { + "name": "github_id", + "ordinal": 6, + "type_info": "Int8" + }, + { + "name": "user_name", + "ordinal": 7, + "type_info": "Varchar" + }, + { + "name": "email", + "ordinal": 8, + "type_info": "Varchar" + }, + { + "name": "avatar_url", + "ordinal": 9, + "type_info": "Varchar" + }, + { + "name": "username", + "ordinal": 10, + "type_info": "Varchar" + }, + { + "name": "bio", + "ordinal": 11, + "type_info": "Varchar" + }, + { + "name": "created", + "ordinal": 12, + "type_info": "Timestamptz" + }, + { + "name": "user_role", + "ordinal": 13, + "type_info": "Varchar" + }, + { + "name": "badges", + "ordinal": 14, + "type_info": "Int8" + }, + { + "name": "balance", + "ordinal": 15, + "type_info": "Numeric" + }, + { + "name": "payout_wallet", + "ordinal": 16, + "type_info": "Varchar" + }, + { + "name": "payout_wallet_type", + "ordinal": 17, + "type_info": "Varchar" + }, + { + "name": "payout_address", + "ordinal": 18, + "type_info": "Varchar" + } + ], + "nullable": [ + false, + false, + false, + false, + false, + false, + true, + true, + true, + true, + false, + true, + false, + false, + false, + false, + true, + true, + true + ], + "parameters": { + "Left": [ + "Int8" + ] + } + }, + "query": "\n SELECT tm.id id, tm.role member_role, tm.permissions permissions, tm.accepted accepted, tm.payouts_split payouts_split,\n u.id user_id, u.github_id github_id, u.name user_name, u.email email,\n u.avatar_url avatar_url, u.username username, u.bio bio,\n u.created created, u.role user_role, u.badges badges, u.balance balance,\n u.payout_wallet payout_wallet, u.payout_wallet_type payout_wallet_type,\n u.payout_address payout_address\n FROM team_members tm\n INNER JOIN users u ON u.id = tm.user_id\n WHERE tm.team_id = $1\n " + }, "d8b4e7e382c77a05395124d5a6a27cccb687d0e2c31b76d49b03aa364d099d42": { "describe": { "columns": [], @@ -6573,86 +6783,6 @@ }, "query": "\n SELECT short, name FROM donation_platforms\n WHERE id = $1\n " }, - "f962e4c6a8bb12afbe25636e26beef2775539c731cc41243b20d779a1fa71e06": { - "describe": { - "columns": [ - { - "name": "id", - "ordinal": 0, - "type_info": "Int8" - }, - { - "name": "github_id", - "ordinal": 1, - "type_info": "Int8" - }, - { - "name": "name", - "ordinal": 2, - "type_info": "Varchar" - }, - { - "name": "email", - "ordinal": 3, - "type_info": "Varchar" - }, - { - "name": "avatar_url", - "ordinal": 4, - "type_info": "Varchar" - }, - { - "name": "username", - "ordinal": 5, - "type_info": "Varchar" - }, - { - "name": "bio", - "ordinal": 6, - "type_info": "Varchar" - }, - { - "name": "created", - "ordinal": 7, - "type_info": "Timestamptz" - }, - { - "name": "role", - "ordinal": 8, - "type_info": "Varchar" - }, - { - "name": "badges", - "ordinal": 9, - "type_info": "Int8" - }, - { - "name": "paypal_email", - "ordinal": 10, - "type_info": "Varchar" - } - ], - "nullable": [ - false, - true, - true, - true, - true, - false, - true, - false, - false, - false, - true - ], - "parameters": { - "Left": [ - "Int8Array" - ] - } - }, - "query": "\n SELECT u.id, u.github_id, u.name, u.email,\n u.avatar_url, u.username, u.bio,\n u.created, u.role, u.badges, u.paypal_email\n FROM users u\n WHERE u.id = ANY($1)\n " - }, "fb955ca41b95120f66c98c0b528b1db10c4be4a55e9641bb104d772e390c9bb7": { "describe": { "columns": [ diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs index dc53e3120..4f36434e9 100644 --- a/src/database/models/team_item.rs +++ b/src/database/models/team_item.rs @@ -1,7 +1,7 @@ use super::ids::*; use crate::database::models::User; use crate::models::teams::Permissions; -use crate::models::users::Badges; +use crate::models::users::{Badges, RecipientType, RecipientWallet}; use rust_decimal::Decimal; pub struct TeamBuilder { @@ -158,7 +158,9 @@ impl TeamMember { SELECT tm.id id, tm.role member_role, tm.permissions permissions, tm.accepted accepted, tm.payouts_split payouts_split, u.id user_id, u.github_id github_id, u.name user_name, u.email email, u.avatar_url avatar_url, u.username username, u.bio bio, - u.created created, u.role user_role, u.badges badges, u.paypal_email paypal_email + u.created created, u.role user_role, u.badges badges, u.balance balance, + u.payout_wallet payout_wallet, u.payout_wallet_type payout_wallet_type, + u.payout_address payout_address FROM team_members tm INNER JOIN users u ON u.id = tm.user_id WHERE tm.team_id = $1 @@ -186,7 +188,10 @@ impl TeamMember { created: m.created, role: m.user_role, badges: Badges::from_bits(m.badges as u64).unwrap_or_default(), - paypal_email: m.paypal_email + balance: m.balance, + payout_wallet: m.payout_wallet.map(|x| RecipientWallet::from_string(&*x)), + payout_wallet_type: m.payout_wallet_type.map(|x| RecipientType::from_string(&*x)), + payout_address: m.payout_address }, payouts_split: m.payouts_split }))) @@ -221,7 +226,9 @@ impl TeamMember { SELECT tm.id id, tm.team_id team_id, tm.role member_role, tm.permissions permissions, tm.accepted accepted, tm.payouts_split payouts_split, u.id user_id, u.github_id github_id, u.name user_name, u.email email, u.avatar_url avatar_url, u.username username, u.bio bio, - u.created created, u.role user_role, u.badges badges, u.paypal_email paypal_email + u.created created, u.role user_role, u.badges badges, u.balance balance, + u.payout_wallet payout_wallet, u.payout_wallet_type payout_wallet_type, + u.payout_address payout_address FROM team_members tm INNER JOIN users u ON u.id = tm.user_id WHERE tm.team_id = ANY($1) @@ -250,7 +257,10 @@ impl TeamMember { created: m.created, role: m.user_role, badges: Badges::from_bits(m.badges as u64).unwrap_or_default(), - paypal_email: m.paypal_email + balance: m.balance, + payout_wallet: m.payout_wallet.map(|x| RecipientWallet::from_string(&*x)), + payout_wallet_type: m.payout_wallet_type.map(|x| RecipientType::from_string(&*x)), + payout_address: m.payout_address }, payouts_split: m.payouts_split }))) diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index 8398f3d4e..c23d4bfd6 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -1,6 +1,7 @@ use super::ids::{ProjectId, UserId}; -use crate::models::users::Badges; +use crate::models::users::{Badges, RecipientType, RecipientWallet}; use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; pub struct User { pub id: UserId, @@ -13,7 +14,10 @@ pub struct User { pub created: DateTime, pub role: String, pub badges: Badges, - pub paypal_email: Option, + pub balance: Decimal, + pub payout_wallet: Option, + pub payout_wallet_type: Option, + pub payout_address: Option, } impl User { @@ -57,7 +61,9 @@ impl User { " SELECT u.github_id, u.name, u.email, u.avatar_url, u.username, u.bio, - u.created, u.role, u.badges, u.paypal_email + u.created, u.role, u.badges, + u.balance, u.payout_wallet, u.payout_wallet_type, + u.payout_address FROM users u WHERE u.id = $1 ", @@ -79,7 +85,14 @@ impl User { role: row.role, badges: Badges::from_bits(row.badges as u64) .unwrap_or_default(), - paypal_email: row.paypal_email, + balance: row.balance, + payout_wallet: row + .payout_wallet + .map(|x| RecipientWallet::from_string(&*x)), + payout_wallet_type: row + .payout_wallet_type + .map(|x| RecipientType::from_string(&*x)), + payout_address: row.payout_address, })) } else { Ok(None) @@ -97,7 +110,9 @@ impl User { " SELECT u.id, u.name, u.email, u.avatar_url, u.username, u.bio, - u.created, u.role, u.badges, u.paypal_email + u.created, u.role, u.badges, + u.balance, u.payout_wallet, u.payout_wallet_type, + u.payout_address FROM users u WHERE u.github_id = $1 ", @@ -119,7 +134,14 @@ impl User { role: row.role, badges: Badges::from_bits(row.badges as u64) .unwrap_or_default(), - paypal_email: row.paypal_email, + balance: row.balance, + payout_wallet: row + .payout_wallet + .map(|x| RecipientWallet::from_string(&*x)), + payout_wallet_type: row + .payout_wallet_type + .map(|x| RecipientType::from_string(&*x)), + payout_address: row.payout_address, })) } else { Ok(None) @@ -137,7 +159,9 @@ impl User { " SELECT u.id, u.github_id, u.name, u.email, u.avatar_url, u.username, u.bio, - u.created, u.role, u.badges, u.paypal_email + u.created, u.role, u.badges, + u.balance, u.payout_wallet, u.payout_wallet_type, + u.payout_address FROM users u WHERE LOWER(u.username) = LOWER($1) ", @@ -159,7 +183,14 @@ impl User { role: row.role, badges: Badges::from_bits(row.badges as u64) .unwrap_or_default(), - paypal_email: row.paypal_email, + balance: row.balance, + payout_wallet: row + .payout_wallet + .map(|x| RecipientWallet::from_string(&*x)), + payout_wallet_type: row + .payout_wallet_type + .map(|x| RecipientType::from_string(&*x)), + payout_address: row.payout_address, })) } else { Ok(None) @@ -181,7 +212,9 @@ impl User { " SELECT u.id, u.github_id, u.name, u.email, u.avatar_url, u.username, u.bio, - u.created, u.role, u.badges, u.paypal_email + u.created, u.role, u.badges, + u.balance, u.payout_wallet, u.payout_wallet_type, + u.payout_address FROM users u WHERE u.id = ANY($1) ", @@ -200,7 +233,14 @@ impl User { created: u.created, role: u.role, badges: Badges::from_bits(u.badges as u64).unwrap_or_default(), - paypal_email: u.paypal_email, + balance: u.balance, + payout_wallet: u + .payout_wallet + .map(|x| RecipientWallet::from_string(&*x)), + payout_wallet_type: u + .payout_wallet_type + .map(|x| RecipientType::from_string(&*x)), + payout_address: u.payout_address, })) }) .try_collect::>() @@ -367,6 +407,16 @@ impl User { .execute(&mut *transaction) .await?; + sqlx::query!( + " + DELETE FROM historical_payouts + WHERE user_id = $1 + ", + id as UserId, + ) + .execute(&mut *transaction) + .await?; + sqlx::query!( " DELETE FROM users diff --git a/src/main.rs b/src/main.rs index 51b41c4ed..b1699738c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ use crate::file_hosting::S3Host; use crate::queue::download::DownloadQueue; +use crate::queue::payouts::PayoutsQueue; use crate::ratelimit::errors::ARError; use crate::ratelimit::memory::{MemoryStore, MemoryStoreActor}; use crate::ratelimit::middleware::RateLimiter; @@ -11,6 +12,7 @@ use log::{error, info, warn}; use search::indexing::index_projects; use search::indexing::IndexingSettings; use std::sync::Arc; +use tokio::sync::Mutex; mod database; mod file_hosting; @@ -157,6 +159,8 @@ async fn main() -> std::io::Result<()> { } }); + let payouts_queue = Arc::new(Mutex::new(PayoutsQueue::new())); + let ip_salt = Pepper { pepper: models::ids::Base62Id(models::ids::random_base62(11)) .to_string(), @@ -215,6 +219,7 @@ async fn main() -> std::io::Result<()> { .app_data(web::Data::new(file_host.clone())) .app_data(web::Data::new(search_config.clone())) .app_data(web::Data::new(download_queue.clone())) + .app_data(web::Data::new(payouts_queue.clone())) .app_data(web::Data::new(ip_salt.clone())) .configure(routes::v1_config) .configure(routes::v2_config) @@ -305,5 +310,9 @@ fn check_env_vars() -> bool { failed |= check_var::("STRIPE_TOKEN"); failed |= check_var::("STRIPE_WEBHOOK_SECRET"); + failed |= check_var::("PAYPAL_API_URL"); + failed |= check_var::("PAYPAL_CLIENT_ID"); + failed |= check_var::("PAYPAL_CLIENT_SECRET"); + failed } diff --git a/src/models/users.rs b/src/models/users.rs index ad547c95d..4d5246c7e 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -1,5 +1,6 @@ use super::ids::Base62Id; use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; #[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -45,7 +46,76 @@ pub struct User { pub created: DateTime, pub role: Role, pub badges: Badges, - pub paypal_email: Option, + pub payout_data: Option, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct UserPayoutData { + pub balance: Decimal, + pub payout_wallet: Option, + pub payout_wallet_type: Option, + pub payout_address: Option, +} + +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum RecipientType { + Email, + Phone, + UserHandle, +} + +impl std::fmt::Display for RecipientType { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + fmt.write_str(self.as_str()) + } +} + +impl RecipientType { + pub fn from_string(string: &str) -> RecipientType { + match string { + "user_handle" => RecipientType::UserHandle, + "phone" => RecipientType::Phone, + _ => RecipientType::Email, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + RecipientType::Email => "email", + RecipientType::Phone => "phone", + RecipientType::UserHandle => "user_handle", + } + } +} + +#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] +#[serde(rename_all = "PascalCase")] +pub enum RecipientWallet { + Venmo, + PayPal, +} + +impl std::fmt::Display for RecipientWallet { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + fmt.write_str(self.as_str()) + } +} + +impl RecipientWallet { + pub fn from_string(string: &str) -> RecipientWallet { + match string { + "venmo" => RecipientWallet::Venmo, + _ => RecipientWallet::PayPal, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + RecipientWallet::PayPal => "paypal", + RecipientWallet::Venmo => "venmo", + } + } } use crate::database::models::user_item::User as DBUser; @@ -62,7 +132,7 @@ impl From for User { created: data.created, role: Role::from_string(&*data.role), badges: data.badges, - paypal_email: None, + payout_data: None, } } } diff --git a/src/queue/mod.rs b/src/queue/mod.rs index 674b799ed..f2d7abb4c 100644 --- a/src/queue/mod.rs +++ b/src/queue/mod.rs @@ -1 +1,2 @@ pub mod download; +pub mod payouts; diff --git a/src/queue/payouts.rs b/src/queue/payouts.rs new file mode 100644 index 000000000..c0c694d69 --- /dev/null +++ b/src/queue/payouts.rs @@ -0,0 +1,138 @@ +use crate::models::users::{RecipientType, RecipientWallet}; +use crate::routes::ApiError; +use chrono::{DateTime, Duration, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::collections::HashMap; + +pub struct PayoutsQueue { + credential: PaypalCredential, + credential_expires: DateTime, +} + +#[derive(Deserialize, Default)] +struct PaypalCredential { + access_token: String, + token_type: String, + expires_in: i64, +} + +#[derive(Serialize)] +pub struct PayoutItem { + pub amount: PayoutAmount, + pub receiver: String, + pub note: String, + pub recipient_type: RecipientType, + pub recipient_wallet: RecipientWallet, + pub sender_item_id: String, +} + +#[derive(Serialize)] +pub struct PayoutAmount { + pub currency: String, + pub value: String, +} + +// Batches payouts and handles token refresh +impl PayoutsQueue { + pub fn new() -> Self { + PayoutsQueue { + credential: Default::default(), + credential_expires: Utc::now() - Duration::days(30), + } + } + + pub async fn refresh_token(&mut self) -> Result<(), ApiError> { + let client = reqwest::Client::new(); + + let combined_key = format!( + "{}:{}", + dotenvy::var("PAYPAL_CLIENT_ID")?, + dotenvy::var("PAYPAL_CLIENT_SECRET")? + ); + let formatted_key = format!("Basic {}", base64::encode(combined_key)); + + let mut form = HashMap::new(); + form.insert("grant_type", "client_credentials"); + + let credential: PaypalCredential = client + .post(&format!("{}oauth2/token", dotenvy::var("PAYPAL_API_URL")?)) + .header("Accept", "application/json") + .header("Accept-Language", "en_US") + .header("Authorization", formatted_key) + .form(&form) + .send() + .await + .map_err(|_| { + ApiError::Payments( + "Error while authenticating with PayPal".to_string(), + ) + })? + .json() + .await + .map_err(|_| { + ApiError::Payments( + "Error while authenticating with PayPal (deser error)" + .to_string(), + ) + })?; + + self.credential_expires = + Utc::now() + Duration::seconds(credential.expires_in); + self.credential = credential; + + Ok(()) + } + + pub async fn send_payout( + &mut self, + payout: PayoutItem, + ) -> Result<(), ApiError> { + if self.credential_expires < Utc::now() { + self.refresh_token().await.map_err(|_| { + ApiError::Payments( + "Error while authenticating with PayPal".to_string(), + ) + })?; + } + + let client = reqwest::Client::new(); + + let res = client.post(&format!("{}payments/payouts", dotenvy::var("PAYPAL_API_URL")?)) + .header("Authorization", format!("{} {}", self.credential.token_type, self.credential.access_token)) + .json(&json! ({ + "sender_batch_header": { + "sender_batch_id": format!("{}-payouts", Utc::now().to_rfc3339()), + "email_subject": "You have received a payment from Modrinth!", + "email_message": "Thank you for creating projects on Modrinth. Please claim this payment within 30 days.", + }, + "items": vec![payout] + })) + .send().await.map_err(|_| ApiError::Payments("Error while sending payout to PayPal".to_string()))?; + + if !res.status().is_success() { + #[derive(Deserialize)] + struct PayPalError { + pub body: PayPalErrorBody, + } + + #[derive(Deserialize)] + struct PayPalErrorBody { + pub message: String, + } + + let body: PayPalError = res.json().await.map_err(|_| { + ApiError::Payments( + "Error while registering payment in PayPal!".to_string(), + ) + })?; + + return Err(ApiError::Payments(format!( + "Error while registering payment in PayPal: {}", + body.body.message + ))); + } + + Ok(()) + } +} diff --git a/src/routes/admin.rs b/src/routes/admin.rs index d81a72ba0..5e3ada204 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -1,10 +1,8 @@ use crate::models::ids::ProjectId; use crate::routes::ApiError; -use crate::util::auth::check_is_admin_from_headers; use crate::util::guards::admin_key_guard; -use crate::util::payout_calc::get_claimable_time; use crate::DownloadQueue; -use actix_web::{get, patch, post, web, HttpRequest, HttpResponse}; +use actix_web::{patch, post, web, HttpResponse}; use chrono::{DateTime, SecondsFormat, Utc}; use rust_decimal::Decimal; use serde::Deserialize; @@ -136,16 +134,6 @@ pub async fn process_payout( ) })?; - sqlx::query!( - " - DELETE FROM payouts_values - WHERE created = $1 - ", - start - ) - .execute(&mut *transaction) - .await?; - struct Project { project_type: String, // user_id, payouts_split @@ -266,22 +254,27 @@ pub async fn process_payout( let project = projects_map.get_mut(&project_id); if let Some(project) = project { - for dependency in dependencies { - let project_multiplier: Decimal = - Decimal::from(dependency.1) / Decimal::from(dep_sum); + if dep_sum > 0 { + for dependency in dependencies { + let project_multiplier: Decimal = + Decimal::from(dependency.1) + / Decimal::from(dep_sum); - if let Some(members) = team_members.get(&dependency.0) { - let members_sum: Decimal = - members.iter().map(|x| x.1).sum(); + if let Some(members) = team_members.get(&dependency.0) { + let members_sum: Decimal = + members.iter().map(|x| x.1).sum(); - for member in members { - let member_multiplier: Decimal = - member.1 / members_sum; - project.split_team_members.push(( - member.0, - member_multiplier * project_multiplier, - project_id, - )); + if members_sum > Decimal::from(0) { + for member in members { + let member_multiplier: Decimal = + member.1 / members_sum; + project.split_team_members.push(( + member.0, + member_multiplier * project_multiplier, + project_id, + )); + } + } } } } @@ -303,50 +296,80 @@ pub async fn process_payout( let sum_tm_splits: Decimal = project.split_team_members.iter().map(|x| x.1).sum(); - for (user_id, split) in project.team_members { - let payout: Decimal = data.amount - * project_multiplier - * (split / sum_splits) - * (if !project.split_team_members.is_empty() { - &split_given - } else { - &default_split_given - }); + if sum_splits > Decimal::from(0) { + for (user_id, split) in project.team_members { + let payout: Decimal = data.amount + * project_multiplier + * (split / sum_splits) + * (if !project.split_team_members.is_empty() { + &split_given + } else { + &default_split_given + }); - if payout > Decimal::from(0) { - sqlx::query!( - " - INSERT INTO payouts_values (user_id, mod_id, amount, created) - VALUES ($1, $2, $3, $4) - ", - user_id, - id, - payout, - start - ) + if payout > Decimal::from(0) { + sqlx::query!( + " + INSERT INTO payouts_values (user_id, mod_id, amount, created) + VALUES ($1, $2, $3, $4) + ", + user_id, + id, + payout, + start + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + UPDATE users + SET balance = balance + $1 + WHERE id = $2 + ", + payout, + user_id + ) .execute(&mut *transaction) .await?; + } } } - for (user_id, split, project_id) in project.split_team_members { - let payout: Decimal = data.amount - * project_multiplier - * (split / sum_tm_splits) - * split_retention; + if sum_tm_splits > Decimal::from(0) { + for (user_id, split, project_id) in project.split_team_members { + let payout: Decimal = data.amount + * project_multiplier + * (split / sum_tm_splits) + * split_retention; - sqlx::query!( - " - INSERT INTO payouts_values (user_id, mod_id, amount, created) - VALUES ($1, $2, $3, $4) - ", - user_id, - project_id, - payout, - start - ) - .execute(&mut *transaction) - .await?; + if payout > Decimal::from(0) { + sqlx::query!( + " + INSERT INTO payouts_values (user_id, mod_id, amount, created) + VALUES ($1, $2, $3, $4) + ", + user_id, + project_id, + payout, + start + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + UPDATE users + SET balance = balance + $1 + WHERE id = $2 + ", + payout, + user_id + ) + .execute(&mut *transaction) + .await?; + } + } } } } @@ -355,35 +378,3 @@ pub async fn process_payout( Ok(HttpResponse::NoContent().body("")) } - -#[get("/_get-payout-data")] -pub async fn get_payout_data( - req: HttpRequest, - pool: web::Data, -) -> Result { - check_is_admin_from_headers(req.headers(), &**pool).await?; - - use futures::stream::TryStreamExt; - - let mut payouts: HashMap = sqlx::query!( - " - SELECT u.paypal_email, SUM(pv.amount) amount - FROM payouts_values pv - INNER JOIN users u ON pv.user_id = u.id AND u.paypal_email IS NOT NULL - WHERE pv.created <= $1 AND pv.claimed = FALSE - GROUP BY u.paypal_email - ", - get_claimable_time(Utc::now(), false) - ) - .fetch_many(&**pool) - .try_filter_map(|e| async { - Ok(e.right().map(|r| (r.paypal_email.unwrap_or_default(), r.amount.unwrap_or_default()))) - }) - .try_collect::>() - .await?; - - let mut minimum_payout = Decimal::from(5); - payouts.retain(|_k, v| v > &mut minimum_payout); - - Ok(HttpResponse::Ok().json(payouts)) -} diff --git a/src/routes/auth.rs b/src/routes/auth.rs index 77bcf78f1..d448ab8ff 100644 --- a/src/routes/auth.rs +++ b/src/routes/auth.rs @@ -22,6 +22,7 @@ use actix_web::http::StatusCode; use actix_web::web::{scope, Data, Query, ServiceConfig}; use actix_web::{get, HttpResponse}; use chrono::Utc; +use rust_decimal::Decimal; use serde::{Deserialize, Serialize}; use sqlx::postgres::PgPool; use thiserror::Error; @@ -273,7 +274,10 @@ pub async fn auth_callback( created: Utc::now(), role: Role::Developer.to_string(), badges: Badges::default(), - paypal_email: None, + balance: Decimal::from(0), + payout_wallet: None, + payout_wallet_type: None, + payout_address: None, } .insert(&mut transaction) .await?; diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 047e0b688..cd723b33f 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -124,7 +124,8 @@ pub fn users_config(cfg: &mut web::ServiceConfig) { .service(users::user_icon_edit) .service(users::user_notifications) .service(users::user_follows) - .service(users::user_payouts), + .service(users::user_payouts) + .service(users::user_payouts_request), ); } @@ -172,8 +173,7 @@ pub fn admin_config(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("admin") .service(admin::count_download) - .service(admin::process_payout) - .service(admin::get_payout_data), + .service(admin::process_payout), ); } diff --git a/src/routes/users.rs b/src/routes/users.rs index 5f559790b..d16482579 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -1,14 +1,16 @@ use crate::database::models::User; use crate::file_hosting::FileHost; use crate::models::notifications::Notification; -use crate::models::projects::{Project, ProjectId, ProjectStatus}; -use crate::models::users::{Badges, Role, UserId}; +use crate::models::projects::{Project, ProjectStatus}; +use crate::models::users::{ + Badges, RecipientType, RecipientWallet, Role, UserId, +}; +use crate::queue::payouts::{PayoutAmount, PayoutItem, PayoutsQueue}; use crate::routes::ApiError; -use crate::util::auth::{check_is_admin_from_headers, get_user_from_headers}; -use crate::util::payout_calc::get_claimable_time; +use crate::util::auth::get_user_from_headers; use crate::util::routes::read_from_payload; use crate::util::validate::validation_errors_to_string; -use actix_web::{delete, get, patch, web, HttpRequest, HttpResponse}; +use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use chrono::{DateTime, Utc}; use lazy_static::lazy_static; use regex::Regex; @@ -17,6 +19,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::PgPool; use std::sync::Arc; +use tokio::sync::Mutex; use validator::Validate; #[get("user")] @@ -157,8 +160,16 @@ pub struct EditUser { pub bio: Option>, pub role: Option, pub badges: Option, - #[validate(email, length(max = 128))] - pub paypal_email: Option>, + #[validate] + pub payout_data: Option, +} + +#[derive(Serialize, Deserialize, Validate)] +pub struct EditPayoutData { + pub payout_wallet: RecipientWallet, + pub payout_wallet_type: RecipientType, + #[validate(length(max = 128))] + pub payout_address: String, } #[patch("{id}")] @@ -303,18 +314,43 @@ pub async fn user_edit( .await?; } - if let Some(paypal_email) = &new_user.paypal_email { + if let Some(payout_data) = &new_user.payout_data { + if payout_data.payout_wallet_type == RecipientType::UserHandle + && payout_data.payout_wallet == RecipientWallet::PayPal + { + return Err(ApiError::InvalidInput( + "You cannot use a paypal wallet with a user handle!" + .to_string(), + )); + } + + if !match payout_data.payout_wallet_type { + RecipientType::Email => { + validator::validate_email(&payout_data.payout_address) + } + RecipientType::Phone => { + validator::validate_phone(&payout_data.payout_address) + } + RecipientType::UserHandle => true, + } { + return Err(ApiError::InvalidInput( + "Invalid wallet specified!".to_string(), + )); + } + sqlx::query!( " UPDATE users - SET paypal_email = $1 - WHERE (id = $2) + SET payout_wallet = $1, payout_wallet_type = $2, payout_address = $3 + WHERE (id = $4) ", - paypal_email.as_deref(), + payout_data.payout_wallet.as_str(), + payout_data.payout_wallet_type.as_str(), + payout_data.payout_address, id as crate::database::models::ids::UserId, ) - .execute(&mut *transaction) - .await?; + .execute(&mut *transaction) + .await?; } transaction.commit().await?; @@ -349,11 +385,8 @@ pub async fn user_icon_edit( let cdn_url = dotenvy::var("CDN_URL")?; let user = get_user_from_headers(req.headers(), &**pool).await?; let id_option = - crate::database::models::User::get_id_from_username_or_id( - &*info.into_inner().0, - &**pool, - ) - .await?; + User::get_id_from_username_or_id(&*info.into_inner().0, &**pool) + .await?; if let Some(id) = id_option { if user.id != id.into() && !user.role.is_mod() { @@ -442,11 +475,9 @@ pub async fn user_delete( removal_type: web::Query, ) -> Result { let user = get_user_from_headers(req.headers(), &**pool).await?; - let id_option = crate::database::models::User::get_id_from_username_or_id( - &*info.into_inner().0, - &**pool, - ) - .await?; + let id_option = + User::get_id_from_username_or_id(&*info.into_inner().0, &**pool) + .await?; if let Some(id) = id_option { if !user.role.is_admin() && user.id != id.into() { @@ -458,10 +489,9 @@ pub async fn user_delete( let mut transaction = pool.begin().await?; let result = if &*removal_type.removal_type == "full" { - crate::database::models::User::remove_full(id, &mut transaction) - .await? + User::remove_full(id, &mut transaction).await? } else { - crate::database::models::User::remove(id, &mut transaction).await? + User::remove(id, &mut transaction).await? }; transaction.commit().await?; @@ -563,11 +593,9 @@ pub async fn user_notifications( #[derive(Serialize)] pub struct Payout { - pub claimed: bool, - pub claimable: bool, pub created: DateTime, - pub project: Option, pub amount: Decimal, + pub status: String, } #[get("{id}/payouts")] @@ -589,39 +617,51 @@ pub async fn user_payouts( )); } - use futures::TryStreamExt; - - let payouts: Vec = sqlx::query!( - " - SELECT pv.mod_id, pv.created, pv.claimed, pv.amount - FROM payouts_values pv - WHERE pv.user_id = $1 - ORDER BY pv.created DESC - ", - id as crate::database::models::UserId - ) - .fetch_many(&**pool) - .try_filter_map(|e| async { - Ok(e.right().map(|row| { - let claimable_time: DateTime = - get_claimable_time(row.created, true); - - Payout { - claimed: row.claimed, - claimable: Utc::now() > claimable_time, + let (all_time, last_month, payouts) = futures::future::try_join3( + sqlx::query!( + " + SELECT SUM(pv.amount) amount + FROM payouts_values pv + WHERE pv.user_id = $1 + ", + id as crate::database::models::UserId + ) + .fetch_one(&**pool), + sqlx::query!( + " + SELECT SUM(pv.amount) amount + FROM payouts_values pv + WHERE pv.user_id = $1 AND created > NOW() - '1 month'::interval + ", + id as crate::database::models::UserId + ) + .fetch_one(&**pool), + sqlx::query!( + " + SELECT hp.created, hp.amount, hp.status + FROM historical_payouts hp + WHERE hp.user_id = $1 + ORDER BY hp.created DESC + ", + id as crate::database::models::UserId + ) + .fetch_many(&**pool) + .try_filter_map(|e| async { + Ok(e.right().map(|row| Payout { created: row.created, - project: row.mod_id.map(|x| ProjectId(x as u64)), amount: row.amount, - } - })) - }) - .try_collect::>() + status: row.status, + })) + }) + .try_collect::>(), + ) .await?; + use futures::TryStreamExt; + Ok(HttpResponse::Ok().json(json!({ - "all_time": payouts.iter().map(|x| x.amount).sum::(), - "current_period": payouts.iter().filter(|x| !x.claimed && !x.claimable).map(|x| x.amount).sum::(), - "withdrawable": payouts.iter().filter(|x| x.claimable && !x.claimed).map(|x| x.amount).sum::(), + "all_time": all_time.amount, + "last_month": last_month.amount, "payouts": payouts, }))) } else { @@ -629,32 +669,97 @@ pub async fn user_payouts( } } -#[get("{id}/payouts")] -pub async fn finish_user_payout( +#[derive(Deserialize)] +pub struct PayoutData { + amount: Decimal, +} + +#[post("{id}/payouts")] +pub async fn user_payouts_request( req: HttpRequest, info: web::Path<(String,)>, pool: web::Data, + data: web::Json, + payouts_queue: web::Data>>, ) -> Result { - check_is_admin_from_headers(req.headers(), &**pool).await?; - + let user = get_user_from_headers(req.headers(), &**pool).await?; let id_option = User::get_id_from_username_or_id(&*info.into_inner().0, &**pool) .await?; if let Some(id) = id_option { - sqlx::query!( - " - UPDATE payouts_values - SET claimed = TRUE - WHERE (claimed = FALSE AND user_id = $1 AND created <= $2) - ", - id as crate::database::models::ids::UserId, - get_claimable_time(Utc::now(), false) - ) - .execute(&**pool) - .await?; + if !user.role.is_admin() && user.id != id.into() { + return Err(ApiError::CustomAuthentication( + "You do not have permission to request payouts of this user!" + .to_string(), + )); + } - Ok(HttpResponse::NoContent().body("")) + if let Some(payouts_data) = user.payout_data { + if let Some(payout_address) = payouts_data.payout_address { + if let Some(payout_wallet_type) = + payouts_data.payout_wallet_type + { + if let Some(payout_wallet) = payouts_data.payout_wallet { + return if data.amount > payouts_data.balance { + let mut transaction = pool.begin().await?; + + sqlx::query!( + " + INSERT INTO historical_payouts (user_id, amount, status) + VALUES ($1, $2, $3) + ", + id as crate::database::models::ids::UserId, + data.amount, + "success" + ) + .execute(&mut *transaction) + .await?; + + sqlx::query!( + " + UPDATE users + SET balance = balance - $1 + WHERE id = $2 + ", + data.amount, + id as crate::database::models::ids::UserId + ) + .execute(&mut *transaction) + .await?; + + let mut payouts_queue = payouts_queue.lock().await; + payouts_queue + .send_payout(PayoutItem { + amount: PayoutAmount { + currency: "USD".to_string(), + value: data.amount.to_string(), + }, + receiver: payout_address, + note: "Payment from Modrinth creator monetization program".to_string(), + recipient_type: payout_wallet_type, + recipient_wallet: payout_wallet, + sender_item_id: format!("{}-{}", UserId::from(id), Utc::now().timestamp()), + }) + .await?; + + transaction.commit().await?; + + Ok(HttpResponse::NoContent().body("")) + } else { + Err(ApiError::InvalidInput( + "You do not have enough funds to make this payout!" + .to_string(), + )) + }; + } + } + } + } + + Err(ApiError::InvalidInput( + "You are not enrolled in the payouts program yet!".to_string(), + )) } else { Ok(HttpResponse::NotFound().body("")) } diff --git a/src/util/auth.rs b/src/util/auth.rs index 40153ec73..52556cda6 100644 --- a/src/util/auth.rs +++ b/src/util/auth.rs @@ -1,7 +1,7 @@ use crate::database; use crate::database::models; use crate::database::models::project_item::QueryProject; -use crate::models::users::{Role, User, UserId}; +use crate::models::users::{Role, User, UserId, UserPayoutData}; use crate::routes::ApiError; use actix_web::http::header::HeaderMap; use actix_web::web; @@ -73,7 +73,12 @@ where created: result.created, role: Role::from_string(&result.role), badges: result.badges, - paypal_email: result.paypal_email, + payout_data: Some(UserPayoutData { + balance: result.balance, + payout_wallet: result.payout_wallet, + payout_wallet_type: result.payout_wallet_type, + payout_address: result.payout_address, + }), }), None => Err(AuthenticationError::InvalidCredentials), } diff --git a/src/util/mod.rs b/src/util/mod.rs index d0f286843..8d648cbf6 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -2,7 +2,6 @@ pub mod auth; pub mod env; pub mod ext; pub mod guards; -pub mod payout_calc; pub mod routes; pub mod validate; pub mod webhook; diff --git a/src/util/payout_calc.rs b/src/util/payout_calc.rs deleted file mode 100644 index 178b21565..000000000 --- a/src/util/payout_calc.rs +++ /dev/null @@ -1,22 +0,0 @@ -use chrono::{DateTime, Datelike, NaiveDate, NaiveDateTime, NaiveTime, Utc}; - -pub fn get_claimable_time( - current: DateTime, - future: bool, -) -> DateTime { - let adder = if current.month() == 1 && !future { - (-1, 12) - } else if current.month() == 12 && future { - (1, 1) - } else { - (0, current.month()) - }; - - DateTime::from_utc( - NaiveDateTime::new( - NaiveDate::from_ymd(current.year() + adder.0, adder.1, 16), - NaiveTime::default(), - ), - Utc, - ) -}