mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2659eeb53 | ||
|
|
d64668698c | ||
|
|
e944a320a1 | ||
|
|
fe29a654a3 |
Generated
+1
@@ -7254,6 +7254,7 @@ dependencies = [
|
||||
"num-traits",
|
||||
"rand 0.8.5",
|
||||
"rkyv",
|
||||
"rust_decimal_macros",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
Generated
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO payouts_values (user_id, amount, created, date_available, affiliate_code_id)\n SELECT * FROM UNNEST ($1::bigint[], $2::numeric[], $3::timestamptz[], $4::timestamptz[], $5::bigint[])\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array",
|
||||
"NumericArray",
|
||||
"TimestamptzArray",
|
||||
"TimestamptzArray",
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "04ce7057512b313803252064aab1b455f943edb16121823b61fb737c14cb8ad3"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n INNER JOIN users_subscriptions us ON us.id = charges.subscription_id\n WHERE\n charges.charge_type = $1 AND\n (\n (charges.status = 'cancelled' AND charges.due < NOW()) OR\n (charges.status = 'expiring' AND charges.due < NOW()) OR\n (charges.status = 'failed' AND charges.last_attempt < NOW() - INTERVAL '2 days')\n )\n AND us.status = 'provisioned'\n ",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n \n INNER JOIN users_subscriptions us ON us.id = charges.subscription_id\n WHERE\n charges.charge_type = $1 AND\n (\n (charges.status = 'cancelled' AND charges.due < NOW()) OR\n (charges.status = 'expiring' AND charges.due < NOW()) OR\n (charges.status = 'failed' AND charges.last_attempt < NOW() - INTERVAL '2 days')\n )\n AND us.status = 'provisioned'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -97,6 +97,11 @@
|
||||
"ordinal": 18,
|
||||
"name": "tax_drift_loss?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "affiliate_code?",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -123,8 +128,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "9f0c73fabe99d9891faaebdd3518b362437dcdcef9cd9a68b950fba61218bb4d"
|
||||
"hash": "08d5ffdd95f844846df8f8b2afce7ed8d1aaeae0822af1a1d19a2cdc80258c41"
|
||||
}
|
||||
+9
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE user_id = $1 ORDER BY due DESC",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n \n\t\t\tWHERE\n\t\t\t status = 'succeeded'\n\t\t\t AND tax_platform_id IS NULL\n AND payment_platform_id IS NOT NULL\n\t\t\tORDER BY due ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n OFFSET $1\n\t\t\tLIMIT $2\n\t\t\t",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -97,10 +97,16 @@
|
||||
"ordinal": 18,
|
||||
"name": "tax_drift_loss?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "affiliate_code?",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
@@ -123,8 +129,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "7973e569e784f416c1b4f1e6f3b099dca9c0d9c84e55951a730d8c214580e0d6"
|
||||
"hash": "17cd6a07a9315599edee66d0bc9c20116546d9af0b14423fb7a16e73c39ba4d8"
|
||||
}
|
||||
+8
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n\t\t\tWHERE\n\t\t\t status = 'succeeded'\n\t\t\t AND tax_platform_id IS NULL\n AND payment_platform_id IS NOT NULL\n\t\t\tORDER BY due ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n OFFSET $1\n\t\t\tLIMIT $2\n\t\t\t",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -97,11 +97,15 @@
|
||||
"ordinal": 18,
|
||||
"name": "tax_drift_loss?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "affiliate_code?",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
@@ -124,8 +128,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "050e755134f6d1f09de805ae2cd0f7ca8f6efb96be9f070c43db7fd2049af2d2"
|
||||
"hash": "1856a9de26a0aa0e6ea5849b7f7ec87a4a56bb203ec57f12ee44adff5eec9f55"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n (\n (status = 'open' AND due < NOW()) OR\n (status = 'failed' AND last_attempt < NOW() - INTERVAL '2 days')\n )\n ",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n status = 'failed' AND due < NOW() - INTERVAL '30 days'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -97,6 +97,11 @@
|
||||
"ordinal": 18,
|
||||
"name": "tax_drift_loss?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "affiliate_code?",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -123,8 +128,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e2e58113bc3a3db6ffc75b5c5e10acd16403aa0679ef53330f2ce3e8a45f7b9f"
|
||||
"hash": "94023ab7dc72dceb9a2cbf17130bce7f18c98e5e82b29aa9dc272ad7173cec51"
|
||||
}
|
||||
Generated
+33
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, parent_charge_id, net, tax_amount, tax_platform_id, tax_last_updated, tax_drift_loss, affiliate_code)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)\n ON CONFLICT (id)\n DO UPDATE\n SET status = EXCLUDED.status,\n last_attempt = EXCLUDED.last_attempt,\n due = EXCLUDED.due,\n subscription_id = EXCLUDED.subscription_id,\n subscription_interval = EXCLUDED.subscription_interval,\n payment_platform = EXCLUDED.payment_platform,\n payment_platform_id = EXCLUDED.payment_platform_id,\n parent_charge_id = EXCLUDED.parent_charge_id,\n net = EXCLUDED.net,\n tax_amount = EXCLUDED.tax_amount,\n tax_platform_id = EXCLUDED.tax_platform_id,\n tax_last_updated = EXCLUDED.tax_last_updated,\n price_id = EXCLUDED.price_id,\n amount = EXCLUDED.amount,\n currency_code = EXCLUDED.currency_code,\n charge_type = EXCLUDED.charge_type,\n\t\t\t\t\ttax_drift_loss = EXCLUDED.tax_drift_loss,\n\t\t\t\t\taffiliate_code = EXCLUDED.affiliate_code\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9b740f4572544085ff84bb95e2fc6c36259ea492a7f830afbaff499ac3072cc0"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE parent_charge_id = $1",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n WHERE user_id = $1 ORDER BY due DESC",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -97,6 +97,11 @@
|
||||
"ordinal": 18,
|
||||
"name": "tax_drift_loss?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "affiliate_code?",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -123,8 +128,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "51c542076b4b3811eb12f051294f55827a27f51e65e668525b8b545f570c0bda"
|
||||
"hash": "9fa81bfef5410d121bf4f275461ec9fd2e2c25d7aae41ba95cc36add3c3f7549"
|
||||
}
|
||||
Generated
-32
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, parent_charge_id, net, tax_amount, tax_platform_id, tax_last_updated, tax_drift_loss)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)\n ON CONFLICT (id)\n DO UPDATE\n SET status = EXCLUDED.status,\n last_attempt = EXCLUDED.last_attempt,\n due = EXCLUDED.due,\n subscription_id = EXCLUDED.subscription_id,\n subscription_interval = EXCLUDED.subscription_interval,\n payment_platform = EXCLUDED.payment_platform,\n payment_platform_id = EXCLUDED.payment_platform_id,\n parent_charge_id = EXCLUDED.parent_charge_id,\n net = EXCLUDED.net,\n tax_amount = EXCLUDED.tax_amount,\n tax_platform_id = EXCLUDED.tax_platform_id,\n tax_last_updated = EXCLUDED.tax_last_updated,\n price_id = EXCLUDED.price_id,\n amount = EXCLUDED.amount,\n currency_code = EXCLUDED.currency_code,\n charge_type = EXCLUDED.charge_type,\n\t\t\t\t\ttax_drift_loss = EXCLUDED.tax_drift_loss\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c0c70ebc3d59a5ab6a4c81e987df178a7828a45ed3134d3336cb59572f40beab"
|
||||
}
|
||||
Generated
-130
@@ -1,130 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n\t\t\tINNER JOIN users u ON u.id = charges.user_id\n\t\t\tWHERE\n\t\t\t status = 'open'\n\t\t\t AND COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) < NOW() - INTERVAL '1 day'\n\t\t\t AND u.email IS NOT NULL\n\t\t\t AND due - INTERVAL '7 days' > NOW()\n AND due - INTERVAL '14 days' < NOW() -- Due between 7 and 14 days from now\n\t\t\tORDER BY COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n\t\t\tLIMIT $1\n\t\t\t",
|
||||
"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": "tax_amount",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "tax_platform_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "subscription_interval?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "payment_platform",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "payment_platform_id?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "parent_charge_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "net?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "tax_last_updated?",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "tax_drift_loss?",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "cd18ae8abe81a159a134923957f4cd6d0ba3b1bcbc89df349cf7b5b1897603b8"
|
||||
}
|
||||
Generated
+136
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n \n\t\t\tINNER JOIN users u ON u.id = charges.user_id\n\t\t\tWHERE\n\t\t\t status = 'open'\n\t\t\t AND COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) < NOW() - INTERVAL '1 day'\n\t\t\t AND u.email IS NOT NULL\n\t\t\t AND due - INTERVAL '7 days' > NOW()\n AND due - INTERVAL '14 days' < NOW() -- Due between 7 and 14 days from now\n\t\t\tORDER BY COALESCE(tax_last_updated, '-infinity' :: TIMESTAMPTZ) ASC\n\t\t\tFOR NO KEY UPDATE SKIP LOCKED\n\t\t\tLIMIT $1\n\t\t\t",
|
||||
"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": "tax_amount",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "tax_platform_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "subscription_interval?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "payment_platform",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "payment_platform_id?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "parent_charge_id?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "net?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "tax_last_updated?",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "tax_drift_loss?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "affiliate_code?",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d02552b5550bc2bd1e1eedc6bbe5f374a6baa7d18a411051c33694f301d2884c"
|
||||
}
|
||||
Generated
+47
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n c.id as charge_id,\n c.affiliate_code,\n c.net,\n ac.affiliate as affiliate_user_id,\n ac.revenue_split\n FROM charges c\n INNER JOIN affiliate_codes ac ON c.affiliate_code = ac.id\n WHERE\n c.status = 'succeeded'\n AND c.net > 0\n AND c.due BETWEEN $1 AND $2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "charge_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "affiliate_code",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "net",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "affiliate_user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "revenue_split",
|
||||
"type_info": "Float8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "d0fda4cc7d8562a9152afa29b3a4903293fc12d78db69808190f0e96a51e860e"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'expiring' OR status = 'cancelled' OR status = 'failed')",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n WHERE id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -97,6 +97,11 @@
|
||||
"ordinal": 18,
|
||||
"name": "tax_drift_loss?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "affiliate_code?",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -123,8 +128,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "9a35729acbba06eafaa205922e4987e082a000ec1b397957650e1332191613ca"
|
||||
"hash": "d853f30019b3afb62cf81c5c8f58f4bb03797e2dbf87f242af76e87af43ed1a9"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n status = 'failed' AND due < NOW() - INTERVAL '30 days'\n ",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n \n WHERE\n charge_type = $1 AND\n (\n (status = 'open' AND due < NOW()) OR\n (status = 'failed' AND last_attempt < NOW() - INTERVAL '2 days')\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -97,6 +97,11 @@
|
||||
"ordinal": 18,
|
||||
"name": "tax_drift_loss?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "affiliate_code?",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -123,8 +128,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "e36e0ac1e2edb73533961a18e913f0b8e4f420a76e511571bb2eed9355771e54"
|
||||
"hash": "fb46fe2707ebcca811a530246e50c1a103b35f2e4ca224e1f3ef448a74531720"
|
||||
}
|
||||
+8
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\"\n FROM charges\n WHERE id = $1",
|
||||
"query": "\n SELECT\n charges.id, charges.user_id, charges.price_id, charges.amount, charges.currency_code, charges.status, charges.due, charges.last_attempt,\n charges.charge_type, charges.subscription_id, charges.tax_amount, charges.tax_platform_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n charges.subscription_interval AS \"subscription_interval?\",\n charges.payment_platform,\n charges.payment_platform_id AS \"payment_platform_id?\",\n charges.parent_charge_id AS \"parent_charge_id?\",\n charges.net AS \"net?\",\n\t\t\t\tcharges.tax_last_updated AS \"tax_last_updated?\",\n\t\t\t\tcharges.tax_drift_loss AS \"tax_drift_loss?\",\n\t\t\t\tcharges.affiliate_code AS \"affiliate_code?\"\n FROM charges\n WHERE parent_charge_id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -97,6 +97,11 @@
|
||||
"ordinal": 18,
|
||||
"name": "tax_drift_loss?",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "affiliate_code?",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -123,8 +128,9 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4e8e9f9cb42f90cc17702386fdb78385608f19dae9439cb6a860503600127b04"
|
||||
"hash": "fffb8f9aeb39df168aaffe48c9054d7578d353459c5046e50ecd5e4aebce6705"
|
||||
}
|
||||
@@ -86,6 +86,7 @@ reqwest = { workspace = true, features = [
|
||||
"rustls-tls-webpki-roots",
|
||||
] }
|
||||
rust_decimal = { workspace = true, features = [
|
||||
"macros",
|
||||
"serde-with-float",
|
||||
"serde-with-str",
|
||||
] }
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE charges
|
||||
ADD COLUMN affiliate_code BIGINT;
|
||||
|
||||
ALTER TABLE payouts_values
|
||||
ADD COLUMN affiliate_code_id BIGINT;
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::queue::affiliate_codes::process_affiliate_code_revenue;
|
||||
use crate::queue::billing::{index_billing, index_subscriptions};
|
||||
use crate::queue::email::EmailQueue;
|
||||
use crate::queue::payouts::{
|
||||
@@ -179,12 +180,17 @@ pub async fn payouts(
|
||||
info!("Started running payouts");
|
||||
let result = process_payout(&pool, &clickhouse).await;
|
||||
if let Err(e) = result {
|
||||
warn!("Payouts run failed: {:?}", e);
|
||||
warn!("Payouts run failed: {e:#?}");
|
||||
}
|
||||
|
||||
let result = index_payouts_notifications(&pool, &redis_pool).await;
|
||||
if let Err(e) = result {
|
||||
warn!("Payouts notifications indexing failed: {:?}", e);
|
||||
warn!("Payouts notifications indexing failed: {e:#?}");
|
||||
}
|
||||
|
||||
let result = process_affiliate_code_revenue(&pool).await;
|
||||
if let Err(e) = result {
|
||||
warn!("Affiliate code revenue processing failed: {e:#?}");
|
||||
}
|
||||
|
||||
info!("Done running payouts");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::database::models::{
|
||||
DBChargeId, DBProductPriceId, DBUserId, DBUserSubscriptionId, DatabaseError,
|
||||
DBAffiliateCodeId, DBChargeId, DBProductPriceId, DBUserId,
|
||||
DBUserSubscriptionId, DatabaseError,
|
||||
};
|
||||
use crate::models::billing::{
|
||||
ChargeStatus, ChargeType, PaymentPlatform, PriceDuration,
|
||||
@@ -34,6 +35,7 @@ pub struct DBCharge {
|
||||
// Net is always in USD
|
||||
pub net: Option<i64>,
|
||||
pub tax_drift_loss: Option<i64>,
|
||||
pub affiliate_code: Option<DBAffiliateCodeId>,
|
||||
}
|
||||
|
||||
struct ChargeQueryResult {
|
||||
@@ -56,6 +58,7 @@ struct ChargeQueryResult {
|
||||
tax_last_updated: Option<DateTime<Utc>>,
|
||||
net: Option<i64>,
|
||||
tax_drift_loss: Option<i64>,
|
||||
affiliate_code: Option<i64>,
|
||||
}
|
||||
|
||||
impl TryFrom<ChargeQueryResult> for DBCharge {
|
||||
@@ -84,6 +87,7 @@ impl TryFrom<ChargeQueryResult> for DBCharge {
|
||||
net: r.net,
|
||||
tax_last_updated: r.tax_last_updated,
|
||||
tax_drift_loss: r.tax_drift_loss,
|
||||
affiliate_code: r.affiliate_code.map(DBAffiliateCodeId),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -103,7 +107,8 @@ macro_rules! select_charges_with_predicate {
|
||||
charges.parent_charge_id AS "parent_charge_id?",
|
||||
charges.net AS "net?",
|
||||
charges.tax_last_updated AS "tax_last_updated?",
|
||||
charges.tax_drift_loss AS "tax_drift_loss?"
|
||||
charges.tax_drift_loss AS "tax_drift_loss?",
|
||||
charges.affiliate_code AS "affiliate_code?"
|
||||
FROM charges
|
||||
"#
|
||||
+ $predicate,
|
||||
@@ -119,8 +124,8 @@ impl DBCharge {
|
||||
) -> Result<DBChargeId, 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, payment_platform, payment_platform_id, parent_charge_id, net, tax_amount, tax_platform_id, tax_last_updated, tax_drift_loss)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||||
INSERT INTO charges (id, user_id, price_id, amount, currency_code, charge_type, status, due, last_attempt, subscription_id, subscription_interval, payment_platform, payment_platform_id, parent_charge_id, net, tax_amount, tax_platform_id, tax_last_updated, tax_drift_loss, affiliate_code)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
|
||||
ON CONFLICT (id)
|
||||
DO UPDATE
|
||||
SET status = EXCLUDED.status,
|
||||
@@ -139,7 +144,8 @@ impl DBCharge {
|
||||
amount = EXCLUDED.amount,
|
||||
currency_code = EXCLUDED.currency_code,
|
||||
charge_type = EXCLUDED.charge_type,
|
||||
tax_drift_loss = EXCLUDED.tax_drift_loss
|
||||
tax_drift_loss = EXCLUDED.tax_drift_loss,
|
||||
affiliate_code = EXCLUDED.affiliate_code
|
||||
"#,
|
||||
self.id.0,
|
||||
self.user_id.0,
|
||||
@@ -160,6 +166,7 @@ impl DBCharge {
|
||||
self.tax_platform_id.as_deref(),
|
||||
self.tax_last_updated,
|
||||
self.tax_drift_loss,
|
||||
self.affiliate_code.map(|x| x.0),
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
.await?;
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ariadne::ids::UserId;
|
||||
use chrono::{Datelike, Duration, TimeZone, Utc};
|
||||
use eyre::Result;
|
||||
use rust_decimal::{Decimal, dec};
|
||||
use sqlx::PgPool;
|
||||
use tracing::{trace, warn};
|
||||
|
||||
use crate::{
|
||||
database::models::{DBAffiliateCodeId, DBUserId},
|
||||
models::ids::AffiliateCodeId,
|
||||
};
|
||||
|
||||
const AFFILIATE_CUT_PERCENTAGE: Decimal = dec!(0.1);
|
||||
|
||||
pub async fn process_affiliate_code_revenue(pool: &PgPool) -> Result<()> {
|
||||
let end_date = Utc::now() - Duration::days(30);
|
||||
let start_date = end_date - Duration::days(30);
|
||||
|
||||
let affiliate_charges = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
c.id as charge_id,
|
||||
c.affiliate_code,
|
||||
c.net,
|
||||
ac.affiliate as affiliate_user_id,
|
||||
ac.revenue_split
|
||||
FROM charges c
|
||||
INNER JOIN affiliate_codes ac ON c.affiliate_code = ac.id
|
||||
WHERE
|
||||
c.status = 'succeeded'
|
||||
AND c.net > 0
|
||||
AND c.due BETWEEN $1 AND $2
|
||||
"#,
|
||||
start_date,
|
||||
end_date
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
if affiliate_charges.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
// Group by affiliate user and affiliate code to create unique rows per affiliate code
|
||||
let mut affiliate_payouts: HashMap<(DBUserId, DBAffiliateCodeId), Decimal> =
|
||||
HashMap::new();
|
||||
|
||||
for charge in affiliate_charges {
|
||||
let Some(net_amount) = charge.net else {
|
||||
continue;
|
||||
};
|
||||
let net_amount = Decimal::new(net_amount, 2);
|
||||
|
||||
let affiliate_user_id = DBUserId(charge.affiliate_user_id);
|
||||
let affiliate_code_id =
|
||||
DBAffiliateCodeId(charge.affiliate_code.unwrap());
|
||||
|
||||
// Use the custom revenue split if specified, otherwise use the default 10%
|
||||
let revenue_split =
|
||||
charge
|
||||
.revenue_split
|
||||
.map_or(AFFILIATE_CUT_PERCENTAGE, |split| {
|
||||
Decimal::from_f64_retain(split)
|
||||
.unwrap_or(AFFILIATE_CUT_PERCENTAGE)
|
||||
});
|
||||
|
||||
if revenue_split < dec!(0) || revenue_split > dec!(1) {
|
||||
warn!(
|
||||
"Charge {} resulted in invalid revenue split {revenue_split}",
|
||||
charge.charge_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let affiliate_cut = net_amount * revenue_split;
|
||||
|
||||
if affiliate_cut > dec!(0) {
|
||||
*affiliate_payouts
|
||||
.entry((affiliate_user_id, affiliate_code_id))
|
||||
.or_insert(dec!(0)) += affiliate_cut;
|
||||
}
|
||||
}
|
||||
|
||||
let mut insert_user_ids = Vec::new();
|
||||
let mut insert_payouts = Vec::new();
|
||||
let mut insert_starts = Vec::new();
|
||||
let mut insert_availables = Vec::new();
|
||||
let mut insert_affiliate_code_ids = Vec::new();
|
||||
|
||||
let created_timestamp = end_date;
|
||||
|
||||
// Affiliate payouts are Net 30 from the end of the processing month
|
||||
let available_timestamp = {
|
||||
let processing_month = end_date.date_naive();
|
||||
let year = processing_month.year();
|
||||
let month = processing_month.month();
|
||||
|
||||
let first_of_next_month = if month == 12 {
|
||||
Utc.with_ymd_and_hms(year + 1, 1, 1, 0, 0, 0).unwrap()
|
||||
} else {
|
||||
Utc.with_ymd_and_hms(year, month + 1, 1, 0, 0, 0).unwrap()
|
||||
};
|
||||
|
||||
first_of_next_month + Duration::days(29)
|
||||
};
|
||||
|
||||
for ((user_id, affiliate_code_id), total_payout) in affiliate_payouts {
|
||||
if total_payout > dec!(0) {
|
||||
insert_user_ids.push(user_id.0);
|
||||
insert_payouts.push(total_payout);
|
||||
insert_starts.push(created_timestamp);
|
||||
insert_availables.push(available_timestamp);
|
||||
insert_affiliate_code_ids.push(affiliate_code_id.0);
|
||||
|
||||
trace!(
|
||||
"User {} gets {total_payout} from affiliate code {}",
|
||||
UserId::from(user_id),
|
||||
AffiliateCodeId::from(affiliate_code_id),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if !insert_user_ids.is_empty() {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO payouts_values (user_id, amount, created, date_available, affiliate_code_id)
|
||||
SELECT * FROM UNNEST ($1::bigint[], $2::numeric[], $3::timestamptz[], $4::timestamptz[], $5::bigint[])
|
||||
"#,
|
||||
&insert_user_ids[..],
|
||||
&insert_payouts[..],
|
||||
&insert_starts[..],
|
||||
&insert_availables[..],
|
||||
&insert_affiliate_code_ids[..],
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -647,6 +647,8 @@ pub async fn try_process_user_redeemal(
|
||||
net: None,
|
||||
tax_last_updated: Some(Utc::now()),
|
||||
tax_drift_loss: Some(0),
|
||||
// Medal redeemals never have an affiliate code.
|
||||
affiliate_code: None,
|
||||
}
|
||||
.upsert(&mut txn)
|
||||
.await?;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod affiliate_codes;
|
||||
pub mod analytics;
|
||||
pub mod billing;
|
||||
pub mod email;
|
||||
|
||||
@@ -4,7 +4,8 @@ use crate::database::models::charge_item::DBCharge;
|
||||
use crate::database::models::notification_item::NotificationBuilder;
|
||||
use crate::database::models::products_tax_identifier_item::product_info_by_product_price_id;
|
||||
use crate::database::models::{
|
||||
charge_item, generate_charge_id, product_item, user_subscription_item,
|
||||
DBAffiliateCodeId, charge_item, generate_charge_id, product_item,
|
||||
user_subscription_item,
|
||||
};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::models::billing::{
|
||||
@@ -12,6 +13,7 @@ use crate::models::billing::{
|
||||
Product, ProductMetadata, ProductPrice, SubscriptionMetadata,
|
||||
SubscriptionStatus, UserSubscription,
|
||||
};
|
||||
use crate::models::ids::AffiliateCodeId;
|
||||
use crate::models::notifications::NotificationBody;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::users::Badges;
|
||||
@@ -347,6 +349,8 @@ pub async fn refund_charge(
|
||||
currency_code: charge.currency_code,
|
||||
tax_last_updated: Some(Utc::now()),
|
||||
tax_drift_loss: Some(0),
|
||||
// Refunds have no affiliate code
|
||||
affiliate_code: None,
|
||||
}
|
||||
.upsert(&mut transaction)
|
||||
.await?;
|
||||
@@ -1282,9 +1286,16 @@ pub enum ChargeRequestType {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct PaymentRequestMetadata {
|
||||
#[serde(flatten)]
|
||||
kind: PaymentRequestMetadataKind,
|
||||
affiliate_code: Option<AffiliateCodeId>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum PaymentRequestMetadata {
|
||||
pub enum PaymentRequestMetadataKind {
|
||||
Pyro {
|
||||
server_name: Option<String>,
|
||||
server_region: Option<String>,
|
||||
@@ -1425,9 +1436,10 @@ pub async fn stripe_webhook(
|
||||
break 'metadata;
|
||||
};
|
||||
|
||||
let payment_metadata = metadata
|
||||
.get(MODRINTH_PAYMENT_METADATA)
|
||||
.and_then(|x| serde_json::from_str(x).ok());
|
||||
let payment_metadata =
|
||||
metadata.get(MODRINTH_PAYMENT_METADATA).and_then(|x| {
|
||||
serde_json::from_str::<PaymentRequestMetadata>(x).ok()
|
||||
});
|
||||
|
||||
let Some(charge_id) = metadata
|
||||
.get(MODRINTH_CHARGE_ID)
|
||||
@@ -1576,13 +1588,13 @@ pub async fn stripe_webhook(
|
||||
|
||||
if intervals.get(&interval).is_some() {
|
||||
let Some(subscription_id) = metadata
|
||||
.get(MODRINTH_SUBSCRIPTION_ID)
|
||||
.and_then(|x| parse_base62(x).ok())
|
||||
.map(|x| {
|
||||
crate::database::models::ids::DBUserSubscriptionId(x as i64)
|
||||
}) else {
|
||||
break 'metadata;
|
||||
};
|
||||
.get(MODRINTH_SUBSCRIPTION_ID)
|
||||
.and_then(|x| parse_base62(x).ok())
|
||||
.map(|x| {
|
||||
crate::database::models::ids::DBUserSubscriptionId(x as i64)
|
||||
}) else {
|
||||
break 'metadata;
|
||||
};
|
||||
|
||||
let subscription = if let Some(mut subscription) = user_subscription_item::DBUserSubscription::get(subscription_id, pool).await? {
|
||||
subscription.status = SubscriptionStatus::Unprovisioned;
|
||||
@@ -1615,6 +1627,11 @@ pub async fn stripe_webhook(
|
||||
}
|
||||
};
|
||||
|
||||
let affiliate_code = payment_metadata
|
||||
.as_ref()
|
||||
.and_then(|m| m.affiliate_code)
|
||||
.map(DBAffiliateCodeId::from);
|
||||
|
||||
let charge = DBCharge {
|
||||
id: charge_id,
|
||||
user_id,
|
||||
@@ -1641,6 +1658,7 @@ pub async fn stripe_webhook(
|
||||
net: None,
|
||||
tax_last_updated: Some(Utc::now()),
|
||||
tax_drift_loss: Some(0),
|
||||
affiliate_code,
|
||||
};
|
||||
|
||||
if charge_status != ChargeStatus::Failed {
|
||||
@@ -1820,12 +1838,12 @@ pub async fn stripe_webhook(
|
||||
} else {
|
||||
let (server_name, server_region, source) =
|
||||
if let Some(
|
||||
PaymentRequestMetadata::Pyro {
|
||||
ref server_name,
|
||||
ref server_region,
|
||||
ref source,
|
||||
PaymentRequestMetadataKind::Pyro {
|
||||
server_name,
|
||||
server_region,
|
||||
source,
|
||||
},
|
||||
) = metadata.payment_metadata
|
||||
) = metadata.payment_metadata.as_ref().map(|m| &m.kind)
|
||||
{
|
||||
(
|
||||
server_name.clone(),
|
||||
@@ -1909,6 +1927,12 @@ pub async fn stripe_webhook(
|
||||
}
|
||||
}
|
||||
|
||||
let affiliate_code = metadata
|
||||
.payment_metadata
|
||||
.as_ref()
|
||||
.and_then(|m| m.affiliate_code)
|
||||
.map(DBAffiliateCodeId::from);
|
||||
|
||||
if let Some(mut subscription) =
|
||||
metadata.user_subscription_item
|
||||
{
|
||||
@@ -2004,6 +2028,7 @@ pub async fn stripe_webhook(
|
||||
tax_platform_id: None,
|
||||
tax_last_updated: Some(Utc::now()),
|
||||
tax_drift_loss: Some(0),
|
||||
affiliate_code,
|
||||
}
|
||||
.upsert(&mut transaction)
|
||||
.await?;
|
||||
|
||||
@@ -0,0 +1,722 @@
|
||||
use chrono::{Datelike, Duration, TimeZone, Utc};
|
||||
use common::{
|
||||
api_v3::ApiV3,
|
||||
environment::{TestEnvironment, with_test_environment},
|
||||
};
|
||||
use labrinth::database::models::{DBAffiliateCodeId, DBUserId};
|
||||
use labrinth::queue::affiliate_codes::process_affiliate_code_revenue;
|
||||
use rust_decimal::dec;
|
||||
|
||||
pub mod common;
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn test_affiliate_code_revenue_processing_default_split() {
|
||||
with_test_environment(
|
||||
None,
|
||||
|test_env: TestEnvironment<ApiV3>| async move {
|
||||
let pool = test_env.db.pool.clone();
|
||||
|
||||
// Create test users
|
||||
let affiliate_user_id = DBUserId(1000);
|
||||
let buyer_user_id = DBUserId(1001);
|
||||
|
||||
// Create test users in the database
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO users (id, username, email, role)
|
||||
VALUES
|
||||
($1, 'affiliate_user', 'affiliate@test.com', 'developer'),
|
||||
($2, 'buyer_user', 'buyer@test.com', 'developer')
|
||||
"#,
|
||||
affiliate_user_id.0,
|
||||
buyer_user_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a dummy product and price for charges to reference
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO products (id, metadata, unitary, name)
|
||||
VALUES (9000, '{}', false, 'Test Product')
|
||||
"#
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO products_prices (id, product_id, currency_code, prices, public)
|
||||
VALUES (9000, 9000, 'USD', '[{"amount": 1000}]', true)
|
||||
"#
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create affiliate code with default split (NULL)
|
||||
let affiliate_code_id = DBAffiliateCodeId(2000);
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, revenue_split)
|
||||
VALUES ($1, NOW(), $2, $2, NULL)
|
||||
"#,
|
||||
affiliate_code_id.0,
|
||||
affiliate_user_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create test charges within the date range
|
||||
let now = Utc::now();
|
||||
let charge_date = now - Duration::days(45); // Within the 30-day window (between 30-60 days ago)
|
||||
|
||||
// Create multiple charges
|
||||
let charges = [
|
||||
1000, // $10.00
|
||||
2000, // $20.00
|
||||
500, // $5.00
|
||||
];
|
||||
|
||||
for (i, amount_cents) in charges.iter().enumerate() {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount)
|
||||
VALUES ($1, $2, 9000, $3, 'USD', $3, 'succeeded', $4, $5, 'recurring', 'stripe', 0)
|
||||
"#,
|
||||
3000 + i as i64, // unique ID
|
||||
buyer_user_id.0,
|
||||
amount_cents,
|
||||
charge_date,
|
||||
affiliate_code_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Process affiliate code revenue
|
||||
process_affiliate_code_revenue(&pool).await.unwrap();
|
||||
|
||||
|
||||
// Verify payouts were created
|
||||
let payouts = sqlx::query!(
|
||||
r#"
|
||||
SELECT user_id, amount, created, date_available, affiliate_code_id
|
||||
FROM payouts_values
|
||||
WHERE user_id = $1 AND affiliate_code_id = $2
|
||||
ORDER BY created DESC
|
||||
"#,
|
||||
affiliate_user_id.0,
|
||||
affiliate_code_id.0
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Should have one payout entry with 10% of total ($3.50)
|
||||
assert_eq!(payouts.len(), 1);
|
||||
assert_eq!(payouts[0].user_id, affiliate_user_id.0);
|
||||
assert_eq!(payouts[0].affiliate_code_id, Some(affiliate_code_id.0));
|
||||
|
||||
// Expected payout: $35.00 * 10% = $3.50
|
||||
let payout_amount = payouts[0].amount;
|
||||
assert_eq!(payout_amount, dec!(3.50));
|
||||
|
||||
// Verify availability date is Net 30
|
||||
let expected_available = {
|
||||
let processing_month = (now - Duration::days(30)).date_naive();
|
||||
let year = processing_month.year();
|
||||
let month = processing_month.month();
|
||||
|
||||
let first_of_next_month = if month == 12 {
|
||||
Utc.with_ymd_and_hms(year + 1, 1, 1, 0, 0, 0).unwrap()
|
||||
} else {
|
||||
Utc.with_ymd_and_hms(year, month + 1, 1, 0, 0, 0).unwrap()
|
||||
};
|
||||
|
||||
first_of_next_month + Duration::days(29)
|
||||
};
|
||||
|
||||
// Allow small time differences due to test execution
|
||||
let time_diff = (payouts[0].date_available - expected_available).num_seconds();
|
||||
assert!(time_diff.abs() < 60);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn test_affiliate_code_revenue_processing_custom_split() {
|
||||
with_test_environment(
|
||||
None,
|
||||
|test_env: TestEnvironment<ApiV3>| async move {
|
||||
let pool = test_env.db.pool.clone();
|
||||
|
||||
// Create test users
|
||||
let affiliate_user_id = DBUserId(2000);
|
||||
let buyer_user_id = DBUserId(2001);
|
||||
|
||||
// Create test users in the database
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO users (id, username, email, role)
|
||||
VALUES
|
||||
($1, 'affiliate_user2', 'affiliate2@test.com', 'developer'),
|
||||
($2, 'buyer_user2', 'buyer2@test.com', 'developer')
|
||||
"#,
|
||||
affiliate_user_id.0,
|
||||
buyer_user_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a dummy product and price for charges to reference
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO products (id, metadata, unitary, name)
|
||||
VALUES (9001, '{}', false, 'Test Product')
|
||||
"#
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO products_prices (id, product_id, currency_code, prices, public)
|
||||
VALUES (9001, 9001, 'USD', '[{"amount": 1000}]', true)
|
||||
"#
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create affiliate code with custom 25% split
|
||||
let affiliate_code_id = DBAffiliateCodeId(2001);
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, revenue_split)
|
||||
VALUES ($1, NOW(), $2, $2, 0.25)
|
||||
"#,
|
||||
affiliate_code_id.0,
|
||||
affiliate_user_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create test charges
|
||||
let now = Utc::now();
|
||||
let charge_date = now - Duration::days(45);
|
||||
|
||||
let charge_amount = 1000; // $10.00
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount)
|
||||
VALUES ($1, $2, 9001, $3, 'USD', $3, 'succeeded', $4, $5, 'recurring', 'stripe', 0)
|
||||
"#,
|
||||
4000, // unique ID
|
||||
buyer_user_id.0,
|
||||
charge_amount,
|
||||
charge_date,
|
||||
affiliate_code_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Process affiliate code revenue
|
||||
process_affiliate_code_revenue(&pool).await.unwrap();
|
||||
|
||||
// Verify payout was created with custom split
|
||||
let payouts = sqlx::query!(
|
||||
r#"
|
||||
SELECT user_id, amount, created, date_available, affiliate_code_id
|
||||
FROM payouts_values
|
||||
WHERE user_id = $1 AND affiliate_code_id = $2
|
||||
"#,
|
||||
affiliate_user_id.0,
|
||||
affiliate_code_id.0
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(payouts.len(), 1);
|
||||
|
||||
// Expected payout: $10.00 * 25% = $2.50
|
||||
let payout_amount = payouts[0].amount;
|
||||
assert_eq!(payout_amount, dec!(2.50));
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn test_affiliate_code_revenue_processing_invalid_split() {
|
||||
with_test_environment(
|
||||
None,
|
||||
|test_env: TestEnvironment<ApiV3>| async move {
|
||||
let pool = test_env.db.pool.clone();
|
||||
|
||||
// Create test users
|
||||
let affiliate_user_id = DBUserId(3000);
|
||||
let buyer_user_id = DBUserId(3001);
|
||||
|
||||
// Create test users in the database
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO users (id, username, email, role)
|
||||
VALUES
|
||||
($1, 'affiliate_user3', 'affiliate3@test.com', 'developer'),
|
||||
($2, 'buyer_user3', 'buyer3@test.com', 'developer')
|
||||
"#,
|
||||
affiliate_user_id.0,
|
||||
buyer_user_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a dummy product and price for charges to reference
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO products (id, metadata, unitary, name)
|
||||
VALUES (9002, '{}', false, 'Test Product')
|
||||
"#
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO products_prices (id, product_id, currency_code, prices, public)
|
||||
VALUES (9002, 9002, 'USD', '[{"amount": 1000}]', true)
|
||||
"#
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create affiliate code with invalid split (150%)
|
||||
let affiliate_code_id = DBAffiliateCodeId(2002);
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, revenue_split)
|
||||
VALUES ($1, NOW(), $2, $2, 1.5)
|
||||
"#,
|
||||
affiliate_code_id.0,
|
||||
affiliate_user_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create test charge
|
||||
let now = Utc::now();
|
||||
let charge_date = now - Duration::days(45);
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount)
|
||||
VALUES ($1, $2, 9002, $3, 'USD', $3, 'succeeded', $4, $5, 'recurring', 'stripe', 0)
|
||||
"#,
|
||||
5000, // unique ID
|
||||
buyer_user_id.0,
|
||||
1000, // $10.00
|
||||
charge_date,
|
||||
affiliate_code_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Process affiliate code revenue - should not create payout due to invalid split
|
||||
process_affiliate_code_revenue(&pool).await.unwrap();
|
||||
|
||||
// Verify no payout was created
|
||||
let payouts = sqlx::query!(
|
||||
r#"
|
||||
SELECT user_id, amount, created, date_available, affiliate_code_id
|
||||
FROM payouts_values
|
||||
WHERE user_id = $1 AND affiliate_code_id = $2
|
||||
"#,
|
||||
affiliate_user_id.0,
|
||||
affiliate_code_id.0
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(payouts.len(), 0);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn test_affiliate_code_revenue_processing_outside_date_range() {
|
||||
with_test_environment(
|
||||
None,
|
||||
|test_env: TestEnvironment<ApiV3>| async move {
|
||||
let pool = test_env.db.pool.clone();
|
||||
|
||||
// Create test users
|
||||
let affiliate_user_id = DBUserId(4000);
|
||||
let buyer_user_id = DBUserId(4001);
|
||||
|
||||
// Create test users in the database
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO users (id, username, email, role)
|
||||
VALUES
|
||||
($1, 'affiliate_user4', 'affiliate4@test.com', 'developer'),
|
||||
($2, 'buyer_user4', 'buyer4@test.com', 'developer')
|
||||
"#,
|
||||
affiliate_user_id.0,
|
||||
buyer_user_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a dummy product and price for charges to reference
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO products (id, metadata, unitary, name)
|
||||
VALUES (9003, '{}', false, 'Test Product')
|
||||
"#
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO products_prices (id, product_id, currency_code, prices, public)
|
||||
VALUES (9003, 9003, 'USD', '[{"amount": 1000}]', true)
|
||||
"#
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create affiliate code
|
||||
let affiliate_code_id = DBAffiliateCodeId(2003);
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, revenue_split)
|
||||
VALUES ($1, NOW(), $2, $2, NULL)
|
||||
"#,
|
||||
affiliate_code_id.0,
|
||||
affiliate_user_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create test charge outside the date range (more than 60 days ago)
|
||||
let old_charge_date = Utc::now() - Duration::days(90);
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount)
|
||||
VALUES ($1, $2, 9003, $3, 'USD', $3, 'succeeded', $4, $5, 'recurring', 'stripe', 0)
|
||||
"#,
|
||||
6000, // unique ID
|
||||
buyer_user_id.0,
|
||||
1000, // $10.00
|
||||
old_charge_date,
|
||||
affiliate_code_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Process affiliate code revenue
|
||||
process_affiliate_code_revenue(&pool).await.unwrap();
|
||||
|
||||
// Verify no payout was created (charge is outside date range)
|
||||
let payouts = sqlx::query!(
|
||||
r#"
|
||||
SELECT user_id, amount, created, date_available, affiliate_code_id
|
||||
FROM payouts_values
|
||||
WHERE user_id = $1 AND affiliate_code_id = $2
|
||||
"#,
|
||||
affiliate_user_id.0,
|
||||
affiliate_code_id.0
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(payouts.len(), 0);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn test_affiliate_code_revenue_processing_failed_charges() {
|
||||
with_test_environment(
|
||||
None,
|
||||
|test_env: TestEnvironment<ApiV3>| async move {
|
||||
let pool = test_env.db.pool.clone();
|
||||
|
||||
// Create test users
|
||||
let affiliate_user_id = DBUserId(5000);
|
||||
let buyer_user_id = DBUserId(5001);
|
||||
|
||||
// Create test users in the database
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO users (id, username, email, role)
|
||||
VALUES
|
||||
($1, 'affiliate_user5', 'affiliate5@test.com', 'developer'),
|
||||
($2, 'buyer_user5', 'buyer5@test.com', 'developer')
|
||||
"#,
|
||||
affiliate_user_id.0,
|
||||
buyer_user_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a dummy product and price for charges to reference
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO products (id, metadata, unitary, name)
|
||||
VALUES (9004, '{}', false, 'Test Product')
|
||||
"#
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO products_prices (id, product_id, currency_code, prices, public)
|
||||
VALUES (9004, 9004, 'USD', '[{"amount": 1000}]', true)
|
||||
"#
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create affiliate code
|
||||
let affiliate_code_id = DBAffiliateCodeId(2004);
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, revenue_split)
|
||||
VALUES ($1, NOW(), $2, $2, NULL)
|
||||
"#,
|
||||
affiliate_code_id.0,
|
||||
affiliate_user_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create test charge with failed status
|
||||
let now = Utc::now();
|
||||
let charge_date = now - Duration::days(45);
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount)
|
||||
VALUES ($1, $2, 9004, $3, 'USD', $3, 'failed', $4, $5, 'recurring', 'stripe', 0)
|
||||
"#,
|
||||
7000, // unique ID
|
||||
buyer_user_id.0,
|
||||
1000, // $10.00
|
||||
charge_date,
|
||||
affiliate_code_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Process affiliate code revenue
|
||||
process_affiliate_code_revenue(&pool).await.unwrap();
|
||||
|
||||
// Verify no payout was created (charge failed)
|
||||
let payouts = sqlx::query!(
|
||||
r#"
|
||||
SELECT user_id, amount, created, date_available, affiliate_code_id
|
||||
FROM payouts_values
|
||||
WHERE user_id = $1 AND affiliate_code_id = $2
|
||||
"#,
|
||||
affiliate_user_id.0,
|
||||
affiliate_code_id.0
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(payouts.len(), 0);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn test_affiliate_code_revenue_processing_multiple_affiliate_codes() {
|
||||
with_test_environment(
|
||||
None,
|
||||
|test_env: TestEnvironment<ApiV3>| async move {
|
||||
let pool = test_env.db.pool.clone();
|
||||
|
||||
// Create test users
|
||||
let affiliate_user_id_1 = DBUserId(6000);
|
||||
let affiliate_user_id_2 = DBUserId(6001);
|
||||
let buyer_user_id = DBUserId(6002);
|
||||
|
||||
// Create test users in the database
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO users (id, username, email, role)
|
||||
VALUES
|
||||
($1, 'affiliate_user6', 'affiliate6@test.com', 'developer'),
|
||||
($2, 'affiliate_user7', 'affiliate7@test.com', 'developer'),
|
||||
($3, 'buyer_user6', 'buyer6@test.com', 'developer')
|
||||
"#,
|
||||
affiliate_user_id_1.0,
|
||||
affiliate_user_id_2.0,
|
||||
buyer_user_id.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create dummy products and prices for charges to reference
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO products (id, metadata, unitary, name)
|
||||
VALUES
|
||||
(9005, '{}', false, 'Test Product'),
|
||||
(9006, '{}', false, 'Test Product')
|
||||
"#
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO products_prices (id, product_id, currency_code, prices, public)
|
||||
VALUES
|
||||
(9005, 9005, 'USD', '[{"amount": 1000}]', true),
|
||||
(9006, 9006, 'USD', '[{"amount": 1000}]', true)
|
||||
"#
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create two affiliate codes for different users
|
||||
let affiliate_code_id_1 = DBAffiliateCodeId(2005);
|
||||
let affiliate_code_id_2 = DBAffiliateCodeId(2006);
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO affiliate_codes (id, created_at, created_by, affiliate, revenue_split)
|
||||
VALUES
|
||||
($1, NOW(), $2, $2, NULL),
|
||||
($3, NOW(), $4, $4, 0.2)
|
||||
"#,
|
||||
affiliate_code_id_1.0,
|
||||
affiliate_user_id_1.0,
|
||||
affiliate_code_id_2.0,
|
||||
affiliate_user_id_2.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create test charges for each affiliate code
|
||||
let now = Utc::now();
|
||||
let charge_date = now - Duration::days(45);
|
||||
|
||||
// Charge for affiliate 1: $20.00
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount)
|
||||
VALUES ($1, $2, 9005, $3, 'USD', $3, 'succeeded', $4, $5, 'recurring', 'stripe', 0)
|
||||
"#,
|
||||
8000, // unique ID
|
||||
buyer_user_id.0,
|
||||
2000, // $20.00
|
||||
charge_date,
|
||||
affiliate_code_id_1.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Charge for affiliate 2: $15.00
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO charges (id, user_id, price_id, amount, currency_code, net, status, due, affiliate_code, charge_type, payment_platform, tax_amount)
|
||||
VALUES ($1, $2, 9006, $3, 'USD', $3, 'succeeded', $4, $5, 'recurring', 'stripe', 0)
|
||||
"#,
|
||||
9000, // unique ID
|
||||
buyer_user_id.0,
|
||||
1500, // $15.00
|
||||
charge_date,
|
||||
affiliate_code_id_2.0
|
||||
)
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Process affiliate code revenue
|
||||
process_affiliate_code_revenue(&pool).await.unwrap();
|
||||
|
||||
// Verify payouts were created for both affiliates
|
||||
let payouts_1 = sqlx::query!(
|
||||
r#"
|
||||
SELECT user_id, amount, created, date_available, affiliate_code_id
|
||||
FROM payouts_values
|
||||
WHERE user_id = $1 AND affiliate_code_id = $2
|
||||
"#,
|
||||
affiliate_user_id_1.0,
|
||||
affiliate_code_id_1.0
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let payouts_2 = sqlx::query!(
|
||||
r#"
|
||||
SELECT user_id, amount, created, date_available, affiliate_code_id
|
||||
FROM payouts_values
|
||||
WHERE user_id = $1 AND affiliate_code_id = $2
|
||||
"#,
|
||||
affiliate_user_id_2.0,
|
||||
affiliate_code_id_2.0
|
||||
)
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Affiliate 1: $20.00 * 10% = $2.00
|
||||
assert_eq!(payouts_1.len(), 1);
|
||||
let payout_amount_1 = payouts_1[0].amount;
|
||||
assert_eq!(payout_amount_1, dec!(2.00));
|
||||
|
||||
// Affiliate 2: $15.00 * 20% = $3.00 (with potential floating point precision)
|
||||
assert_eq!(payouts_2.len(), 1);
|
||||
let payout_amount_2 = payouts_2[0].amount;
|
||||
// The actual calculation may have tiny precision differences due to f64 -> Decimal conversion
|
||||
assert!(payout_amount_2 > dec!(2.99) && payout_amount_2 < dec!(3.01));
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Reference in New Issue
Block a user