You've already forked AstralRinth
forked from didirus/AstralRinth
Move charges to DB + fix subscription recurring payments (#971)
* Move charges to DB + fix subscription recurring payments * Finish most + pyro integration * Finish billing * Run prepare * Fix intervals * Fix clippy * Remove unused test
This commit is contained in:
4
.env
4
.env
@@ -103,4 +103,6 @@ FLAME_ANVIL_URL=none
|
||||
STRIPE_API_KEY=none
|
||||
STRIPE_WEBHOOK_SECRET=none
|
||||
|
||||
ADITUDE_API_KEY=none
|
||||
ADITUDE_API_KEY=none
|
||||
|
||||
PYRO_API_KEY=none
|
||||
82
.sqlx/query-285cdd452fff85480dde02119d224a6e422e4041deb6f640ab5159d55ba2789c.json
generated
Normal file
82
.sqlx/query-285cdd452fff85480dde02119d224a6e422e4041deb6f640ab5159d55ba2789c.json
generated
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"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')",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "price_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "amount",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "currency_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "due",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "last_attempt",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "charge_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "subscription_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "subscription_interval",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "285cdd452fff85480dde02119d224a6e422e4041deb6f640ab5159d55ba2789c"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n id, user_id, price_id, interval, created, expires, last_charge, status\n FROM users_subscriptions\n WHERE expires < $1",
|
||||
"query": "\n SELECT\n us.id, us.user_id, us.price_id, us.interval, us.created, us.status, us.metadata\n FROM users_subscriptions us\n \n INNER JOIN charges c\n ON c.subscription_id = us.id\n AND (\n (c.status = 'cancelled' AND c.due < $1) OR\n (c.status = 'failed' AND c.last_attempt < $1 - INTERVAL '2 days')\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -30,18 +30,13 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "expires",
|
||||
"type_info": "Timestamptz"
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "last_charge",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
"name": "metadata",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -56,9 +51,8 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "61a87b00baaba022ab32eedf177d5b9dc6d5b7568cf1df15fac6c9e85acfa448"
|
||||
"hash": "3cbc34bc326595fc9d070494613fca57628eed279f720565fab55c8d10decd88"
|
||||
}
|
||||
82
.sqlx/query-457493bd11254ba1c9f81c47f15e8154053ae4e90e319d34a940fb73e33a69d4.json
generated
Normal file
82
.sqlx/query-457493bd11254ba1c9f81c47f15e8154053ae4e90e319d34a940fb73e33a69d4.json
generated
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"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",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "price_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "amount",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "currency_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "due",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "last_attempt",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "charge_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "subscription_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "subscription_interval",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "457493bd11254ba1c9f81c47f15e8154053ae4e90e319d34a940fb73e33a69d4"
|
||||
}
|
||||
24
.sqlx/query-56d7b62fc05c77f228e46dbfe4eaca81b445a7f5a44e52a0526a1b57bd7a8c9d.json
generated
Normal file
24
.sqlx/query-56d7b62fc05c77f228e46dbfe4eaca81b445a7f5a44e52a0526a1b57bd7a8c9d.json
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
82
.sqlx/query-86ee460c74f0052a4945ab4df9829b3b077930d8e9e09dca76fde8983413adc6.json
generated
Normal file
82
.sqlx/query-86ee460c74f0052a4945ab4df9829b3b077930d8e9e09dca76fde8983413adc6.json
generated
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"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",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "price_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "amount",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "currency_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "due",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "last_attempt",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "charge_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "subscription_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "subscription_interval",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "86ee460c74f0052a4945ab4df9829b3b077930d8e9e09dca76fde8983413adc6"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM users_subscriptions\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "88d135700420321a3896f9262bb663df0ac672d465d78445e48f321fc47e09cb"
|
||||
}
|
||||
20
.sqlx/query-8bcf4589c429ab0abf2460f658fd91caafb733a5217b832ab9dcf7fde60d49dd.json
generated
Normal file
20
.sqlx/query-8bcf4589c429ab0abf2460f658fd91caafb733a5217b832ab9dcf7fde60d49dd.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO users_subscriptions (\n id, user_id, price_id, interval, created, status, metadata\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7\n )\n ON CONFLICT (id)\n DO UPDATE\n SET interval = EXCLUDED.interval,\n status = EXCLUDED.status,\n price_id = EXCLUDED.price_id,\n metadata = EXCLUDED.metadata\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Varchar",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8bcf4589c429ab0abf2460f658fd91caafb733a5217b832ab9dcf7fde60d49dd"
|
||||
}
|
||||
22
.sqlx/query-95b52480a3fc7257a95e1cbc0e05f13c7934e3019675c04d9d3f240eb590bdc4.json
generated
Normal file
22
.sqlx/query-95b52480a3fc7257a95e1cbc0e05f13c7934e3019675c04d9d3f240eb590bdc4.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM charges WHERE id=$1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "95b52480a3fc7257a95e1cbc0e05f13c7934e3019675c04d9d3f240eb590bdc4"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n id, user_id, price_id, interval, created, expires, last_charge, status\n FROM users_subscriptions\n WHERE id = ANY($1::bigint[])",
|
||||
"query": "\n SELECT\n us.id, us.user_id, us.price_id, us.interval, us.created, us.status, us.metadata\n FROM users_subscriptions us\n WHERE us.id = ANY($1::bigint[])",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -30,18 +30,13 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "expires",
|
||||
"type_info": "Timestamptz"
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "last_charge",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
"name": "metadata",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -56,9 +51,8 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "07afad3b85ed64acbe9584570fdec92f923abf17439f0011e2b46797cec0ad97"
|
||||
"hash": "a25ee30b6968dc98b66b1beac5124f39c64ad8815ff0ec0a98903fee0b4167c7"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n id, user_id, price_id, interval, created, expires, last_charge, status\n FROM users_subscriptions\n WHERE user_id = $1",
|
||||
"query": "\n SELECT\n us.id, us.user_id, us.price_id, us.interval, us.created, us.status, us.metadata\n FROM users_subscriptions us\n WHERE us.user_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -30,18 +30,13 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "expires",
|
||||
"type_info": "Timestamptz"
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "last_charge",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
"name": "metadata",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -56,9 +51,8 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d6d3c29ff2aa3b311a19225cefcd5b8844fbe5bedf44ffe24f31b12e5bc5f868"
|
||||
"hash": "af1a10f0fa88c7893cff3a451fd890762fd7068cab7822a5b60545b44e6ba775"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM users_subscriptions\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b64651865cf9c1fbebed7f188da6566d53049176d72073c22a04b43adea18326"
|
||||
}
|
||||
14
.sqlx/query-bc2a2166718a2d2b23e57cde6b144e88f58fd2e1cc3e6da8d90708cbf242f761.json
generated
Normal file
14
.sqlx/query-bc2a2166718a2d2b23e57cde6b144e88f58fd2e1cc3e6da8d90708cbf242f761.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n DELETE FROM charges\n WHERE id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bc2a2166718a2d2b23e57cde6b144e88f58fd2e1cc3e6da8d90708cbf242f761"
|
||||
}
|
||||
15
.sqlx/query-bd26a27ce80ca796ae19bc709c92800a0a43dfef4a37a5725403d33ccb20d908.json
generated
Normal file
15
.sqlx/query-bd26a27ce80ca796ae19bc709c92800a0a43dfef4a37a5725403d33ccb20d908.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE users\n SET badges = $1\n WHERE (id = $2)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "bd26a27ce80ca796ae19bc709c92800a0a43dfef4a37a5725403d33ccb20d908"
|
||||
}
|
||||
82
.sqlx/query-e68e27fcb3e85233be06e7435aaeb6b27d8dbe2ddaf211ba37a026eab3bb6926.json
generated
Normal file
82
.sqlx/query-e68e27fcb3e85233be06e7435aaeb6b27d8dbe2ddaf211ba37a026eab3bb6926.json
generated
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"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')",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "price_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "amount",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "currency_code",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "due",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "last_attempt",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "charge_type",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "subscription_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "subscription_interval",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e68e27fcb3e85233be06e7435aaeb6b27d8dbe2ddaf211ba37a026eab3bb6926"
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO users_subscriptions (\n id, user_id, price_id, interval, created, expires, last_charge, status\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8\n )\n ON CONFLICT (id)\n DO UPDATE\n SET interval = EXCLUDED.interval,\n expires = EXCLUDED.expires,\n last_charge = EXCLUDED.last_charge,\n status = EXCLUDED.status,\n price_id = EXCLUDED.price_id\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e82ba8bafb4e45b8a8a100c639a9174f196a50cd74c9243ddd57d6f4f3d0b062"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE users\n SET badges = $1\n WHERE (id = $2)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f643ba5f92e5f76cc2f9d2016f52dc03483c1e578dd5ea39119fcf5ad58d8250"
|
||||
}
|
||||
17
migrations/20240923163452_charges-fix.sql
Normal file
17
migrations/20240923163452_charges-fix.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
CREATE TABLE charges (
|
||||
id bigint PRIMARY KEY,
|
||||
user_id bigint REFERENCES users NOT NULL,
|
||||
price_id bigint REFERENCES products_prices NOT NULL,
|
||||
amount bigint NOT NULL,
|
||||
currency_code text NOT NULL,
|
||||
status varchar(255) NOT NULL,
|
||||
due timestamptz DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
last_attempt timestamptz NULL,
|
||||
charge_type text NOT NULL,
|
||||
subscription_id bigint NULL,
|
||||
subscription_interval text NULL
|
||||
);
|
||||
|
||||
ALTER TABLE users_subscriptions DROP COLUMN last_charge;
|
||||
ALTER TABLE users_subscriptions ADD COLUMN metadata jsonb NULL;
|
||||
ALTER TABLE users_subscriptions DROP COLUMN expires;
|
||||
181
src/database/models/charge_item.rs
Normal file
181
src/database/models/charge_item.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use crate::database::models::{
|
||||
ChargeId, DatabaseError, ProductPriceId, UserId, UserSubscriptionId,
|
||||
};
|
||||
use crate::models::billing::{ChargeStatus, ChargeType, PriceDuration};
|
||||
use chrono::{DateTime, Utc};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
pub struct ChargeItem {
|
||||
pub id: ChargeId,
|
||||
pub user_id: UserId,
|
||||
pub price_id: ProductPriceId,
|
||||
pub amount: i64,
|
||||
pub currency_code: String,
|
||||
pub status: ChargeStatus,
|
||||
pub due: DateTime<Utc>,
|
||||
pub last_attempt: Option<DateTime<Utc>>,
|
||||
|
||||
pub type_: ChargeType,
|
||||
pub subscription_id: Option<UserSubscriptionId>,
|
||||
pub subscription_interval: Option<PriceDuration>,
|
||||
}
|
||||
|
||||
struct ChargeResult {
|
||||
id: i64,
|
||||
user_id: i64,
|
||||
price_id: i64,
|
||||
amount: i64,
|
||||
currency_code: String,
|
||||
status: String,
|
||||
due: DateTime<Utc>,
|
||||
last_attempt: Option<DateTime<Utc>>,
|
||||
charge_type: String,
|
||||
subscription_id: Option<i64>,
|
||||
subscription_interval: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<ChargeResult> for ChargeItem {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(r: ChargeResult) -> Result<Self, Self::Error> {
|
||||
Ok(ChargeItem {
|
||||
id: ChargeId(r.id),
|
||||
user_id: UserId(r.user_id),
|
||||
price_id: ProductPriceId(r.price_id),
|
||||
amount: r.amount,
|
||||
currency_code: r.currency_code,
|
||||
status: ChargeStatus::from_string(&r.status),
|
||||
due: r.due,
|
||||
last_attempt: r.last_attempt,
|
||||
type_: ChargeType::from_string(&r.charge_type),
|
||||
subscription_id: r.subscription_id.map(UserSubscriptionId),
|
||||
subscription_interval: r
|
||||
.subscription_interval
|
||||
.map(|x| PriceDuration::from_string(&x)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! select_charges_with_predicate {
|
||||
($predicate:tt, $param:ident) => {
|
||||
sqlx::query_as!(
|
||||
ChargeResult,
|
||||
r#"
|
||||
SELECT id, user_id, price_id, amount, currency_code, status, due, last_attempt, charge_type, subscription_id, subscription_interval
|
||||
FROM charges
|
||||
"#
|
||||
+ $predicate,
|
||||
$param
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
impl ChargeItem {
|
||||
pub async fn upsert(
|
||||
&self,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<ChargeId, DatabaseError> {
|
||||
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)
|
||||
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
|
||||
"#,
|
||||
self.id.0,
|
||||
self.user_id.0,
|
||||
self.price_id.0,
|
||||
self.amount,
|
||||
self.currency_code,
|
||||
self.type_.as_str(),
|
||||
self.status.as_str(),
|
||||
self.due,
|
||||
self.last_attempt,
|
||||
self.subscription_id.map(|x| x.0),
|
||||
self.subscription_interval.map(|x| x.as_str()),
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
Ok(self.id)
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
id: ChargeId,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Option<ChargeItem>, DatabaseError> {
|
||||
let id = id.0;
|
||||
let res = select_charges_with_predicate!("WHERE id = $1", id)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(res.and_then(|r| r.try_into().ok()))
|
||||
}
|
||||
|
||||
pub async fn get_from_user(
|
||||
user_id: UserId,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Vec<ChargeItem>, DatabaseError> {
|
||||
let user_id = user_id.0;
|
||||
let res = select_charges_with_predicate!("WHERE user_id = $1 ORDER BY due DESC", user_id)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
Ok(res
|
||||
.into_iter()
|
||||
.map(|r| r.try_into())
|
||||
.collect::<Result<Vec<_>, serde_json::Error>>()?)
|
||||
}
|
||||
|
||||
pub async fn get_open_subscription(
|
||||
user_subscription_id: UserSubscriptionId,
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Option<ChargeItem>, DatabaseError> {
|
||||
let user_subscription_id = user_subscription_id.0;
|
||||
let res = select_charges_with_predicate!(
|
||||
"WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled')",
|
||||
user_subscription_id
|
||||
)
|
||||
.fetch_optional(exec)
|
||||
.await?;
|
||||
|
||||
Ok(res.and_then(|r| r.try_into().ok()))
|
||||
}
|
||||
|
||||
pub async fn get_chargeable(
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Vec<ChargeItem>, DatabaseError> {
|
||||
let now = Utc::now();
|
||||
|
||||
let res = select_charges_with_predicate!("WHERE (status = 'open' AND due < $1) OR (status = 'failed' AND last_attempt < $1 - INTERVAL '2 days')", now)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
Ok(res
|
||||
.into_iter()
|
||||
.map(|r| r.try_into())
|
||||
.collect::<Result<Vec<_>, serde_json::Error>>()?)
|
||||
}
|
||||
|
||||
pub async fn remove(
|
||||
id: ChargeId,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM charges
|
||||
WHERE id = $1
|
||||
",
|
||||
id.0 as i64
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -256,6 +256,14 @@ generate_ids!(
|
||||
UserSubscriptionId
|
||||
);
|
||||
|
||||
generate_ids!(
|
||||
pub generate_charge_id,
|
||||
ChargeId,
|
||||
8,
|
||||
"SELECT EXISTS(SELECT 1 FROM charges WHERE id=$1)",
|
||||
ChargeId
|
||||
);
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Type, Hash, Serialize, Deserialize)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct UserId(pub i64);
|
||||
@@ -386,6 +394,10 @@ pub struct ProductPriceId(pub i64);
|
||||
#[sqlx(transparent)]
|
||||
pub struct UserSubscriptionId(pub i64);
|
||||
|
||||
#[derive(Copy, Clone, Debug, Type, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||
#[sqlx(transparent)]
|
||||
pub struct ChargeId(pub i64);
|
||||
|
||||
use crate::models::ids;
|
||||
|
||||
impl From<ids::ProjectId> for ProjectId {
|
||||
@@ -571,3 +583,14 @@ impl From<UserSubscriptionId> for ids::UserSubscriptionId {
|
||||
ids::UserSubscriptionId(id.0 as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ids::ChargeId> for ChargeId {
|
||||
fn from(id: ids::ChargeId) -> Self {
|
||||
ChargeId(id.0 as i64)
|
||||
}
|
||||
}
|
||||
impl From<ChargeId> for ids::ChargeId {
|
||||
fn from(id: ChargeId) -> Self {
|
||||
ids::ChargeId(id.0 as u64)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod categories;
|
||||
pub mod charge_item;
|
||||
pub mod collection_item;
|
||||
pub mod flow_item;
|
||||
pub mod ids;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::database::models::{DatabaseError, ProductPriceId, UserId, UserSubscriptionId};
|
||||
use crate::models::billing::{PriceDuration, SubscriptionStatus};
|
||||
use crate::models::billing::{PriceDuration, SubscriptionMetadata, SubscriptionStatus};
|
||||
use chrono::{DateTime, Utc};
|
||||
use itertools::Itertools;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
pub struct UserSubscriptionItem {
|
||||
pub id: UserSubscriptionId,
|
||||
@@ -9,9 +10,8 @@ pub struct UserSubscriptionItem {
|
||||
pub price_id: ProductPriceId,
|
||||
pub interval: PriceDuration,
|
||||
pub created: DateTime<Utc>,
|
||||
pub expires: DateTime<Utc>,
|
||||
pub last_charge: Option<DateTime<Utc>>,
|
||||
pub status: SubscriptionStatus,
|
||||
pub metadata: Option<SubscriptionMetadata>,
|
||||
}
|
||||
|
||||
struct UserSubscriptionResult {
|
||||
@@ -20,9 +20,8 @@ struct UserSubscriptionResult {
|
||||
price_id: i64,
|
||||
interval: String,
|
||||
pub created: DateTime<Utc>,
|
||||
pub expires: DateTime<Utc>,
|
||||
pub last_charge: Option<DateTime<Utc>>,
|
||||
pub status: String,
|
||||
pub metadata: serde_json::Value,
|
||||
}
|
||||
|
||||
macro_rules! select_user_subscriptions_with_predicate {
|
||||
@@ -31,8 +30,8 @@ macro_rules! select_user_subscriptions_with_predicate {
|
||||
UserSubscriptionResult,
|
||||
r#"
|
||||
SELECT
|
||||
id, user_id, price_id, interval, created, expires, last_charge, status
|
||||
FROM users_subscriptions
|
||||
us.id, us.user_id, us.price_id, us.interval, us.created, us.status, us.metadata
|
||||
FROM users_subscriptions us
|
||||
"#
|
||||
+ $predicate,
|
||||
$param
|
||||
@@ -40,18 +39,19 @@ macro_rules! select_user_subscriptions_with_predicate {
|
||||
};
|
||||
}
|
||||
|
||||
impl From<UserSubscriptionResult> for UserSubscriptionItem {
|
||||
fn from(r: UserSubscriptionResult) -> Self {
|
||||
UserSubscriptionItem {
|
||||
impl TryFrom<UserSubscriptionResult> for UserSubscriptionItem {
|
||||
type Error = serde_json::Error;
|
||||
|
||||
fn try_from(r: UserSubscriptionResult) -> Result<Self, Self::Error> {
|
||||
Ok(UserSubscriptionItem {
|
||||
id: UserSubscriptionId(r.id),
|
||||
user_id: UserId(r.user_id),
|
||||
price_id: ProductPriceId(r.price_id),
|
||||
interval: PriceDuration::from_string(&r.interval),
|
||||
created: r.created,
|
||||
expires: r.expires,
|
||||
last_charge: r.last_charge,
|
||||
status: SubscriptionStatus::from_string(&r.status),
|
||||
}
|
||||
metadata: serde_json::from_value(r.metadata)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,11 +70,14 @@ impl UserSubscriptionItem {
|
||||
let ids = ids.iter().map(|id| id.0).collect_vec();
|
||||
let ids_ref: &[i64] = &ids;
|
||||
let results =
|
||||
select_user_subscriptions_with_predicate!("WHERE id = ANY($1::bigint[])", ids_ref)
|
||||
select_user_subscriptions_with_predicate!("WHERE us.id = ANY($1::bigint[])", ids_ref)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
Ok(results.into_iter().map(|r| r.into()).collect())
|
||||
Ok(results
|
||||
.into_iter()
|
||||
.map(|r| r.try_into())
|
||||
.collect::<Result<Vec<_>, serde_json::Error>>()?)
|
||||
}
|
||||
|
||||
pub async fn get_all_user(
|
||||
@@ -82,22 +85,38 @@ impl UserSubscriptionItem {
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Vec<UserSubscriptionItem>, DatabaseError> {
|
||||
let user_id = user_id.0;
|
||||
let results = select_user_subscriptions_with_predicate!("WHERE user_id = $1", user_id)
|
||||
let results = select_user_subscriptions_with_predicate!("WHERE us.user_id = $1", user_id)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
Ok(results.into_iter().map(|r| r.into()).collect())
|
||||
Ok(results
|
||||
.into_iter()
|
||||
.map(|r| r.try_into())
|
||||
.collect::<Result<Vec<_>, serde_json::Error>>()?)
|
||||
}
|
||||
|
||||
pub async fn get_all_expired(
|
||||
pub async fn get_all_unprovision(
|
||||
exec: impl sqlx::Executor<'_, Database = sqlx::Postgres>,
|
||||
) -> Result<Vec<UserSubscriptionItem>, DatabaseError> {
|
||||
let now = Utc::now();
|
||||
let results = select_user_subscriptions_with_predicate!("WHERE expires < $1", now)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
let results = select_user_subscriptions_with_predicate!(
|
||||
"
|
||||
INNER JOIN charges c
|
||||
ON c.subscription_id = us.id
|
||||
AND (
|
||||
(c.status = 'cancelled' AND c.due < $1) OR
|
||||
(c.status = 'failed' AND c.last_attempt < $1 - INTERVAL '2 days')
|
||||
)
|
||||
",
|
||||
now
|
||||
)
|
||||
.fetch_all(exec)
|
||||
.await?;
|
||||
|
||||
Ok(results.into_iter().map(|r| r.into()).collect())
|
||||
Ok(results
|
||||
.into_iter()
|
||||
.map(|r| r.try_into())
|
||||
.collect::<Result<Vec<_>, serde_json::Error>>()?)
|
||||
}
|
||||
|
||||
pub async fn upsert(
|
||||
@@ -107,44 +126,25 @@ impl UserSubscriptionItem {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO users_subscriptions (
|
||||
id, user_id, price_id, interval, created, expires, last_charge, status
|
||||
id, user_id, price_id, interval, created, status, metadata
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8
|
||||
$1, $2, $3, $4, $5, $6, $7
|
||||
)
|
||||
ON CONFLICT (id)
|
||||
DO UPDATE
|
||||
SET interval = EXCLUDED.interval,
|
||||
expires = EXCLUDED.expires,
|
||||
last_charge = EXCLUDED.last_charge,
|
||||
status = EXCLUDED.status,
|
||||
price_id = EXCLUDED.price_id
|
||||
price_id = EXCLUDED.price_id,
|
||||
metadata = EXCLUDED.metadata
|
||||
",
|
||||
self.id.0,
|
||||
self.user_id.0,
|
||||
self.price_id.0,
|
||||
self.interval.as_str(),
|
||||
self.created,
|
||||
self.expires,
|
||||
self.last_charge,
|
||||
self.status.as_str(),
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove(
|
||||
id: UserSubscriptionId,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
sqlx::query!(
|
||||
"
|
||||
DELETE FROM users_subscriptions
|
||||
WHERE id = $1
|
||||
",
|
||||
id.0 as i64
|
||||
serde_json::to_value(&self.metadata)?,
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
@@ -15,7 +15,7 @@ pub async fn delete_file_version(
|
||||
file_name: &str,
|
||||
) -> Result<DeleteFileData, FileHostingError> {
|
||||
let response = reqwest::Client::new()
|
||||
.post(&format!(
|
||||
.post(format!(
|
||||
"{}/b2api/v2/b2_delete_file_version",
|
||||
authorization_data.api_url
|
||||
))
|
||||
|
||||
29
src/lib.rs
29
src/lib.rs
@@ -259,15 +259,24 @@ pub fn app_setup(
|
||||
}
|
||||
|
||||
let stripe_client = stripe::Client::new(dotenvy::var("STRIPE_API_KEY").unwrap());
|
||||
// {
|
||||
// let pool_ref = pool.clone();
|
||||
// let redis_ref = redis_pool.clone();
|
||||
// let stripe_client_ref = stripe_client.clone();
|
||||
//
|
||||
// actix_rt::spawn(async move {
|
||||
// routes::internal::billing::task(stripe_client_ref, pool_ref, redis_ref).await;
|
||||
// });
|
||||
// }
|
||||
{
|
||||
let pool_ref = pool.clone();
|
||||
let redis_ref = redis_pool.clone();
|
||||
let stripe_client_ref = stripe_client.clone();
|
||||
|
||||
actix_rt::spawn(async move {
|
||||
routes::internal::billing::task(stripe_client_ref, pool_ref, redis_ref).await;
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let pool_ref = pool.clone();
|
||||
let redis_ref = redis_pool.clone();
|
||||
|
||||
actix_rt::spawn(async move {
|
||||
routes::internal::billing::subscription_task(pool_ref, redis_ref).await;
|
||||
});
|
||||
}
|
||||
|
||||
let ip_salt = Pepper {
|
||||
pepper: models::ids::Base62Id(models::ids::random_base62(11)).to_string(),
|
||||
@@ -456,5 +465,7 @@ pub fn check_env_vars() -> bool {
|
||||
|
||||
failed |= check_var::<u64>("ADITUDE_API_KEY");
|
||||
|
||||
failed |= check_var::<String>("PYRO_API_KEY");
|
||||
|
||||
failed
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ pub struct Product {
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum ProductMetadata {
|
||||
Midas,
|
||||
Pyro { ram: u32 },
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
@@ -55,6 +56,13 @@ pub enum PriceDuration {
|
||||
}
|
||||
|
||||
impl PriceDuration {
|
||||
pub fn duration(&self) -> chrono::Duration {
|
||||
match self {
|
||||
PriceDuration::Monthly => chrono::Duration::days(30),
|
||||
PriceDuration::Yearly => chrono::Duration::days(365),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> PriceDuration {
|
||||
match string {
|
||||
"monthly" => PriceDuration::Monthly,
|
||||
@@ -84,8 +92,7 @@ pub struct UserSubscription {
|
||||
pub interval: PriceDuration,
|
||||
pub status: SubscriptionStatus,
|
||||
pub created: DateTime<Utc>,
|
||||
pub expires: DateTime<Utc>,
|
||||
pub last_charge: Option<DateTime<Utc>>,
|
||||
pub metadata: Option<SubscriptionMetadata>,
|
||||
}
|
||||
|
||||
impl From<crate::database::models::user_subscription_item::UserSubscriptionItem>
|
||||
@@ -99,38 +106,119 @@ impl From<crate::database::models::user_subscription_item::UserSubscriptionItem>
|
||||
interval: x.interval,
|
||||
status: x.status,
|
||||
created: x.created,
|
||||
expires: x.expires,
|
||||
last_charge: x.last_charge,
|
||||
metadata: x.metadata,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SubscriptionStatus {
|
||||
Active,
|
||||
PaymentProcessing,
|
||||
PaymentFailed,
|
||||
Cancelled,
|
||||
Provisioned,
|
||||
Unprovisioned,
|
||||
}
|
||||
|
||||
impl SubscriptionStatus {
|
||||
pub fn from_string(string: &str) -> SubscriptionStatus {
|
||||
match string {
|
||||
"active" => SubscriptionStatus::Active,
|
||||
"payment-processing" => SubscriptionStatus::PaymentProcessing,
|
||||
"payment-failed" => SubscriptionStatus::PaymentFailed,
|
||||
"cancelled" => SubscriptionStatus::Cancelled,
|
||||
_ => SubscriptionStatus::Cancelled,
|
||||
"provisioned" => SubscriptionStatus::Provisioned,
|
||||
"unprovisioned" => SubscriptionStatus::Unprovisioned,
|
||||
_ => SubscriptionStatus::Provisioned,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
SubscriptionStatus::Active => "active",
|
||||
SubscriptionStatus::PaymentProcessing => "payment-processing",
|
||||
SubscriptionStatus::PaymentFailed => "payment-failed",
|
||||
SubscriptionStatus::Cancelled => "cancelled",
|
||||
SubscriptionStatus::Provisioned => "provisioned",
|
||||
SubscriptionStatus::Unprovisioned => "unprovisioned",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum SubscriptionMetadata {
|
||||
Pyro { id: String },
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
#[serde(from = "Base62Id")]
|
||||
#[serde(into = "Base62Id")]
|
||||
pub struct ChargeId(pub u64);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Charge {
|
||||
pub id: ChargeId,
|
||||
pub user_id: UserId,
|
||||
pub price_id: ProductPriceId,
|
||||
pub amount: i64,
|
||||
pub currency_code: String,
|
||||
pub status: ChargeStatus,
|
||||
pub due: DateTime<Utc>,
|
||||
pub last_attempt: Option<DateTime<Utc>>,
|
||||
#[serde(flatten)]
|
||||
pub type_: ChargeType,
|
||||
pub subscription_id: Option<UserSubscriptionId>,
|
||||
pub subscription_interval: Option<PriceDuration>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum ChargeType {
|
||||
OneTime,
|
||||
Subscription,
|
||||
Proration,
|
||||
}
|
||||
|
||||
impl ChargeType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ChargeType::OneTime => "one-time",
|
||||
ChargeType::Subscription { .. } => "subscription",
|
||||
ChargeType::Proration { .. } => "proration",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_string(string: &str) -> ChargeType {
|
||||
match string {
|
||||
"one-time" => ChargeType::OneTime,
|
||||
"subscription" => ChargeType::Subscription,
|
||||
"proration" => ChargeType::Proration,
|
||||
_ => ChargeType::OneTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Eq, PartialEq, Copy, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum ChargeStatus {
|
||||
// Open charges are for the next billing interval
|
||||
Open,
|
||||
Processing,
|
||||
Succeeded,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl ChargeStatus {
|
||||
pub fn from_string(string: &str) -> ChargeStatus {
|
||||
match string {
|
||||
"processing" => ChargeStatus::Processing,
|
||||
"succeeded" => ChargeStatus::Succeeded,
|
||||
"failed" => ChargeStatus::Failed,
|
||||
"open" => ChargeStatus::Open,
|
||||
"cancelled" => ChargeStatus::Cancelled,
|
||||
_ => ChargeStatus::Failed,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ChargeStatus::Processing => "processing",
|
||||
ChargeStatus::Succeeded => "succeeded",
|
||||
ChargeStatus::Failed => "failed",
|
||||
ChargeStatus::Open => "open",
|
||||
ChargeStatus::Cancelled => "cancelled",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@ pub use super::teams::TeamId;
|
||||
pub use super::threads::ThreadId;
|
||||
pub use super::threads::ThreadMessageId;
|
||||
pub use super::users::UserId;
|
||||
pub use crate::models::billing::UserSubscriptionId;
|
||||
pub use crate::models::v3::billing::{ProductId, ProductPriceId};
|
||||
pub use crate::models::billing::{ChargeId, ProductId, ProductPriceId, UserSubscriptionId};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||
@@ -137,6 +136,7 @@ base62_id_impl!(PayoutId, PayoutId);
|
||||
base62_id_impl!(ProductId, ProductId);
|
||||
base62_id_impl!(ProductPriceId, ProductPriceId);
|
||||
base62_id_impl!(UserSubscriptionId, UserSubscriptionId);
|
||||
base62_id_impl!(ChargeId, ChargeId);
|
||||
|
||||
pub mod base62_impl {
|
||||
use serde::de::{self, Deserializer, Visitor};
|
||||
|
||||
@@ -74,7 +74,7 @@ impl PayoutsQueue {
|
||||
}
|
||||
|
||||
let credential: PaypalCredential = client
|
||||
.post(&format!("{}oauth2/token", dotenvy::var("PAYPAL_API_URL")?))
|
||||
.post(format!("{}oauth2/token", dotenvy::var("PAYPAL_API_URL")?))
|
||||
.header("Accept", "application/json")
|
||||
.header("Accept-Language", "en_US")
|
||||
.header("Authorization", formatted_key)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -510,7 +510,7 @@ impl AuthProvider {
|
||||
map.insert("grant_type", "authorization_code");
|
||||
|
||||
let token: AccessToken = reqwest::Client::new()
|
||||
.post(&format!("{api_url}oauth2/token"))
|
||||
.post(format!("{api_url}oauth2/token"))
|
||||
.header(reqwest::header::ACCEPT, "application/json")
|
||||
.header(
|
||||
AUTHORIZATION,
|
||||
@@ -766,7 +766,7 @@ impl AuthProvider {
|
||||
let api_url = dotenvy::var("PAYPAL_API_URL")?;
|
||||
|
||||
let paypal_user: PayPalUser = reqwest::Client::new()
|
||||
.get(&format!(
|
||||
.get(format!(
|
||||
"{api_url}identity/openidconnect/userinfo?schema=openid"
|
||||
))
|
||||
.header(reqwest::header::USER_AGENT, "Modrinth")
|
||||
@@ -1393,7 +1393,7 @@ pub async fn sign_up_beehiiv(email: &str) -> Result<(), AuthenticationError> {
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
.post(&format!(
|
||||
.post(format!(
|
||||
"https://api.beehiiv.com/v2/publications/{id}/subscriptions"
|
||||
))
|
||||
.header(AUTHORIZATION, format!("Bearer {}", api_key))
|
||||
|
||||
@@ -21,7 +21,7 @@ pub async fn export(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&*session_queue,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
)
|
||||
.await?
|
||||
@@ -34,19 +34,19 @@ pub async fn export(
|
||||
crate::database::models::Collection::get_many(&collection_ids, &**pool, &redis)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::collections::Collection::from(x))
|
||||
.map(crate::models::collections::Collection::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let follows = crate::database::models::User::get_follows(user_id, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::ids::ProjectId::from(x))
|
||||
.map(crate::models::ids::ProjectId::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let projects = crate::database::models::User::get_projects(user_id, &**pool, &redis)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::ids::ProjectId::from(x))
|
||||
.map(crate::models::ids::ProjectId::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let org_ids = crate::database::models::User::get_organizations(user_id, &**pool).await?;
|
||||
@@ -64,7 +64,7 @@ pub async fn export(
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::notifications::Notification::from(x))
|
||||
.map(crate::models::notifications::Notification::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let oauth_clients =
|
||||
@@ -73,7 +73,7 @@ pub async fn export(
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::oauth_clients::OAuthClient::from(x))
|
||||
.map(crate::models::oauth_clients::OAuthClient::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let oauth_authorizations = crate::database::models::oauth_client_authorization_item::OAuthClientAuthorization::get_all_for_user(
|
||||
@@ -81,7 +81,7 @@ pub async fn export(
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::oauth_clients::OAuthClientAuthorization::from(x))
|
||||
.map(crate::models::oauth_clients::OAuthClientAuthorization::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let pat_ids = crate::database::models::pat_item::PersonalAccessToken::get_user_pats(
|
||||
@@ -102,7 +102,7 @@ pub async fn export(
|
||||
let payouts = crate::database::models::payout_item::Payout::get_many(&payout_ids, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::payouts::Payout::from(x))
|
||||
.map(crate::models::payouts::Payout::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let report_ids =
|
||||
@@ -110,7 +110,7 @@ pub async fn export(
|
||||
let reports = crate::database::models::report_item::Report::get_many(&report_ids, &**pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::reports::Report::from(x))
|
||||
.map(crate::models::reports::Report::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let message_ids = sqlx::query!(
|
||||
@@ -146,7 +146,7 @@ pub async fn export(
|
||||
crate::database::models::image_item::Image::get_many(&uploaded_images_ids, &**pool, &redis)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::images::Image::from(x))
|
||||
.map(crate::models::images::Image::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let subscriptions =
|
||||
@@ -155,7 +155,7 @@ pub async fn export(
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| crate::models::billing::UserSubscription::from(x))
|
||||
.map(crate::models::billing::UserSubscription::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
|
||||
@@ -106,14 +106,11 @@ pub async fn version_create(
|
||||
|
||||
// Get all possible side-types for loaders given- we will use these to check if we need to convert/apply singleplayer, etc.
|
||||
let loaders = match v3::tags::loader_list(client.clone(), redis.clone()).await {
|
||||
Ok(loader_response) => match v2_reroute::extract_ok_json::<
|
||||
Vec<v3::tags::LoaderData>,
|
||||
>(loader_response)
|
||||
.await
|
||||
{
|
||||
Ok(loaders) => loaders,
|
||||
Err(_) => vec![],
|
||||
},
|
||||
Ok(loader_response) => {
|
||||
(v2_reroute::extract_ok_json::<Vec<v3::tags::LoaderData>>(loader_response)
|
||||
.await)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
Err(_) => vec![],
|
||||
};
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ pub async fn create_payout(
|
||||
.fetch_optional(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
let balance = get_user_balance(user.id.into(), &**pool).await?;
|
||||
let balance = get_user_balance(user.id, &pool).await?;
|
||||
if balance.available < body.amount || body.amount < Decimal::ZERO {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You do not have enough funds to make this payout!".to_string(),
|
||||
@@ -734,7 +734,7 @@ pub async fn get_balance(
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let balance = get_user_balance(user.id.into(), &**pool).await?;
|
||||
let balance = get_user_balance(user.id.into(), &pool).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(balance))
|
||||
}
|
||||
|
||||
@@ -579,94 +579,6 @@ pub async fn test_bulk_edit_links() {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn delete_project_with_report() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let api = &test_env.api;
|
||||
let alpha_project_id: &str = &test_env.dummy.project_alpha.project_id;
|
||||
let beta_project_id: &str = &test_env.dummy.project_beta.project_id;
|
||||
|
||||
// Create a report for the project
|
||||
let resp = api
|
||||
.create_report(
|
||||
"copyright",
|
||||
alpha_project_id,
|
||||
CommonItemType::Project,
|
||||
"Hey! This is my project, copied without permission!",
|
||||
ENEMY_USER_PAT, // Enemy makes a report
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
let value = test::read_body_json::<serde_json::Value, _>(resp).await;
|
||||
let alpha_report_id = value["id"].as_str().unwrap();
|
||||
|
||||
// Confirm existence
|
||||
let resp = api
|
||||
.get_report(
|
||||
alpha_report_id,
|
||||
ENEMY_USER_PAT, // Enemy makes a report
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
// Do the same for beta
|
||||
let resp = api
|
||||
.create_report(
|
||||
"copyright",
|
||||
beta_project_id,
|
||||
CommonItemType::Project,
|
||||
"Hey! This is my project, copied without permission!",
|
||||
ENEMY_USER_PAT, // Enemy makes a report
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
let value = test::read_body_json::<serde_json::Value, _>(resp).await;
|
||||
let beta_report_id = value["id"].as_str().unwrap();
|
||||
|
||||
// Delete the project
|
||||
let resp = api.remove_project(alpha_project_id, USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// Confirm that the project is gone from the cache
|
||||
let mut redis_pool = test_env.db.redis_pool.connect().await.unwrap();
|
||||
assert_eq!(
|
||||
redis_pool
|
||||
.get(PROJECTS_SLUGS_NAMESPACE, "demo")
|
||||
.await
|
||||
.unwrap()
|
||||
.and_then(|x| x.parse::<i64>().ok()),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
redis_pool
|
||||
.get(PROJECTS_SLUGS_NAMESPACE, alpha_project_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.and_then(|x| x.parse::<i64>().ok()),
|
||||
None
|
||||
);
|
||||
|
||||
// Report for alpha no longer exists
|
||||
let resp = api
|
||||
.get_report(
|
||||
alpha_report_id,
|
||||
ENEMY_USER_PAT, // Enemy makes a report
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NOT_FOUND);
|
||||
|
||||
// Confirm that report for beta still exists
|
||||
let resp = api
|
||||
.get_report(
|
||||
beta_report_id,
|
||||
ENEMY_USER_PAT, // Enemy makes a report
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn permissions_patch_project_v3() {
|
||||
with_test_environment(Some(8), |test_env: TestEnvironment<ApiV3>| async move {
|
||||
|
||||
Reference in New Issue
Block a user