Compare commits

...
24 changed files with 1239 additions and 203 deletions
Generated
+1
View File
@@ -7254,6 +7254,7 @@ dependencies = [
"num-traits",
"rand 0.8.5",
"rkyv",
"rust_decimal_macros",
"serde",
"serde_json",
]
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
+1
View File
@@ -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;
+8 -2
View File
@@ -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?;
+146
View File
@@ -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(())
}
+2
View File
@@ -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
View File
@@ -1,3 +1,4 @@
pub mod affiliate_codes;
pub mod analytics;
pub mod billing;
pub mod email;
+42 -17
View File
@@ -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;
}