mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 12:05:53 +00:00
Add provisional payout estimates
This commit is contained in:
+1
-1
@@ -379,7 +379,7 @@ export function getMetricValue(
|
|||||||
return point.metric_kind === 'playtime' ? point.seconds : 0
|
return point.metric_kind === 'playtime' ? point.seconds : 0
|
||||||
case 'revenue': {
|
case 'revenue': {
|
||||||
if (point.metric_kind !== 'revenue') return 0
|
if (point.metric_kind !== 'revenue') return 0
|
||||||
const value = Number.parseFloat(point.revenue)
|
const value = point.revenue
|
||||||
return Number.isFinite(value) ? value : 0
|
return Number.isFinite(value) ? value : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -337,7 +337,7 @@ function addAnalyticsMetricToTableRow(
|
|||||||
row.playtime += point.seconds
|
row.playtime += point.seconds
|
||||||
break
|
break
|
||||||
case 'revenue': {
|
case 'revenue': {
|
||||||
const parsed = Number.parseFloat(point.revenue)
|
const parsed = point.revenue
|
||||||
row.revenue += Number.isFinite(parsed) ? parsed : 0
|
row.revenue += Number.isFinite(parsed) ? parsed : 0
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -519,7 +519,7 @@ export function computeTotals(
|
|||||||
totals.playtime += dataPoint.seconds
|
totals.playtime += dataPoint.seconds
|
||||||
break
|
break
|
||||||
case 'revenue': {
|
case 'revenue': {
|
||||||
const value = Number.parseFloat(dataPoint.revenue)
|
const value = dataPoint.revenue
|
||||||
totals.revenue += Number.isFinite(value) ? value : 0
|
totals.revenue += Number.isFinite(value) ? value : 0
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+34
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT\n date_available AS \"date_available!\",\n SUM(amount) FILTER (WHERE NOT provisional) finalized_sum,\n SUM(amount) FILTER (WHERE provisional) estimate_sum\n FROM (\n SELECT date_available, amount, FALSE provisional\n FROM payouts_values\n WHERE user_id = $1\n\n UNION ALL\n\n SELECT date_available, amount, TRUE provisional\n FROM payout_estimates\n WHERE user_id = $1\n ) payout_amounts\n GROUP BY date_available\n ORDER BY date_available DESC\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "date_available!",
|
||||||
|
"type_info": "Timestamptz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "finalized_sum",
|
||||||
|
"type_info": "Numeric"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "estimate_sum",
|
||||||
|
"type_info": "Numeric"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "01ac9da2281dd65c1b4a3eb5a2b941ba117decea2e953f400bbcabb1ad873a3f"
|
||||||
|
}
|
||||||
Generated
+15
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n INSERT INTO payout_estimates (\n period,\n user_id,\n mod_id,\n amount,\n created,\n date_available\n )\n VALUES ('2000-01-01', $1, $2, 1, NOW(), NOW())\n ON CONFLICT DO NOTHING\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Int8"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "1bd41cf314799ee2d7ded70463f8c32acf008386040415fe472994341b64eb81"
|
||||||
|
}
|
||||||
Generated
-28
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "\n SELECT date_available, SUM(amount) sum\n FROM payouts_values\n WHERE user_id = $1\n GROUP BY date_available\n ORDER BY date_available DESC\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"ordinal": 0,
|
|
||||||
"name": "date_available",
|
|
||||||
"type_info": "Timestamptz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 1,
|
|
||||||
"name": "sum",
|
|
||||||
"type_info": "Numeric"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Int8"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
false,
|
|
||||||
null
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "58fbda9daed27c5c466849a944ab3df193679cef17e8588ac3be79978b60bdab"
|
|
||||||
}
|
|
||||||
Generated
+17
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n INSERT INTO payout_estimates (\n period,\n user_id,\n mod_id,\n amount,\n created,\n date_available\n )\n VALUES ('2000-01-01', $1, $2, 75, $3, $4)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Int8",
|
||||||
|
"Timestamptz",
|
||||||
|
"Timestamptz"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "657cc50d96f6a6d46aba09a6957175e698fd9f221bbcfb45c13d15b5f24498b2"
|
||||||
|
}
|
||||||
Generated
+19
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n INSERT INTO payouts_values (\n user_id,\n mod_id,\n amount,\n created,\n date_available\n )\n VALUES\n ($1, $2, 100, $3, $4),\n ($1, $2, 20, $5, $6)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Int8",
|
||||||
|
"Int8",
|
||||||
|
"Timestamptz",
|
||||||
|
"Timestamptz",
|
||||||
|
"Timestamptz",
|
||||||
|
"Timestamptz"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "9f831b44a53edd68073c4e695f65bc1a380a30bfd19d26a4b2a4b61fbccee86b"
|
||||||
|
}
|
||||||
+9
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT\n WIDTH_BUCKET(\n EXTRACT(EPOCH FROM created)::bigint,\n EXTRACT(EPOCH FROM $1::timestamp with time zone AT TIME ZONE 'UTC')::bigint,\n EXTRACT(EPOCH FROM $2::timestamp with time zone AT TIME ZONE 'UTC')::bigint,\n $3::integer\n ) AS \"bucket?\",\n mod_id AS \"mod_id?\",\n CASE\n WHEN $5 AND ($6 OR mod_id = ANY($7)) THEN user_id\n ELSE 0\n END AS \"user_id?\",\n SUM(amount) AS \"amount_sum?\"\n FROM payouts_values\n WHERE\n -- only project revenue is counted here\n -- for affiliate code revenue, see `affiliate_code_revenue`\n payouts_values.mod_id IS NOT NULL\n AND payouts_values.mod_id = ANY($4)\n AND created >= $1\n AND created < $2\n GROUP BY 1, 2, 3\n ",
|
"query": "\n SELECT\n WIDTH_BUCKET(\n EXTRACT(EPOCH FROM created)::bigint,\n EXTRACT(EPOCH FROM $1::timestamp with time zone AT TIME ZONE 'UTC')::bigint,\n EXTRACT(EPOCH FROM $2::timestamp with time zone AT TIME ZONE 'UTC')::bigint,\n $3::integer\n ) AS \"bucket?\",\n mod_id AS \"mod_id?\",\n CASE\n WHEN $5 AND ($6 OR mod_id = ANY($7)) THEN user_id\n ELSE 0\n END AS \"user_id?\",\n estimated AS \"estimated!\",\n SUM(amount) AS \"amount_sum?\"\n FROM (\n SELECT user_id, mod_id, amount, created, FALSE AS estimated\n FROM payouts_values\n WHERE mod_id IS NOT NULL\n\n UNION ALL\n\n SELECT user_id, mod_id, amount, created, TRUE AS estimated\n FROM payout_estimates\n ) creator_revenue\n WHERE\n -- only project revenue is counted here\n -- for affiliate code revenue, see `affiliate_code_revenue`\n creator_revenue.mod_id = ANY($4)\n AND created >= $1\n AND created < $2\n GROUP BY 1, 2, 3, 4\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -20,6 +20,11 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
|
"name": "estimated!",
|
||||||
|
"type_info": "Bool"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
"name": "amount_sum?",
|
"name": "amount_sum?",
|
||||||
"type_info": "Numeric"
|
"type_info": "Numeric"
|
||||||
}
|
}
|
||||||
@@ -37,10 +42,11 @@
|
|||||||
},
|
},
|
||||||
"nullable": [
|
"nullable": [
|
||||||
null,
|
null,
|
||||||
true,
|
null,
|
||||||
|
null,
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "c2e40b00e2764be89a162feb1f1cc927e873e51a0d3b6e3e142efa3ba9e0e76f"
|
"hash": "b290fce93574e1cadb76a01a74720bd45b980294b84853eda559f16da49c5cc3"
|
||||||
}
|
}
|
||||||
Generated
+21
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n INSERT INTO payout_estimates (\n period,\n user_id,\n mod_id,\n amount,\n created,\n date_available\n )\n VALUES\n ($1, $2, $3, 30, $4, $5),\n ($6, $2, $3, 40, $7, $8)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Date",
|
||||||
|
"Int8",
|
||||||
|
"Int8",
|
||||||
|
"Timestamptz",
|
||||||
|
"Timestamptz",
|
||||||
|
"Date",
|
||||||
|
"Timestamptz",
|
||||||
|
"Timestamptz"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "b2a1ba3ea43ffc1c99fd8147d810396ef978c33ff68730c06c70edb15e8f3833"
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE payout_estimates (
|
||||||
|
period DATE NOT NULL
|
||||||
|
CHECK (EXTRACT(DAY FROM period) = 1),
|
||||||
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
mod_id BIGINT NOT NULL REFERENCES mods(id) ON DELETE CASCADE,
|
||||||
|
amount NUMERIC(40, 20) NOT NULL
|
||||||
|
CHECK (amount >= 0),
|
||||||
|
created TIMESTAMPTZ NOT NULL,
|
||||||
|
date_available TIMESTAMPTZ NOT NULL,
|
||||||
|
calculated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
PRIMARY KEY (period, user_id, mod_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX payout_estimates_user_id
|
||||||
|
ON payout_estimates (user_id);
|
||||||
|
|
||||||
|
CREATE INDEX payout_estimates_mod_id
|
||||||
|
ON payout_estimates (mod_id);
|
||||||
|
|
||||||
|
CREATE INDEX payout_estimates_created
|
||||||
|
ON payout_estimates (created);
|
||||||
@@ -40,6 +40,8 @@ pub struct AffiliateCodeRevenueFilters {
|
|||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct AffiliateCodeRevenue {
|
pub struct AffiliateCodeRevenue {
|
||||||
/// Total revenue for this bucket.
|
/// Total revenue for this bucket.
|
||||||
|
#[serde(with = "rust_decimal::serde::float")]
|
||||||
|
#[schema(value_type = f64)]
|
||||||
pub revenue: Decimal,
|
pub revenue: Decimal,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub use project_playtime::{
|
|||||||
pub(crate) use project_revenue::fetch as fetch_project_revenue;
|
pub(crate) use project_revenue::fetch as fetch_project_revenue;
|
||||||
pub use project_revenue::{
|
pub use project_revenue::{
|
||||||
ProjectRevenue, ProjectRevenueField, ProjectRevenueFilters,
|
ProjectRevenue, ProjectRevenueField, ProjectRevenueFilters,
|
||||||
|
ProjectRevenueKind,
|
||||||
};
|
};
|
||||||
pub(crate) use project_views::fetch as fetch_project_views;
|
pub(crate) use project_views::fetch as fetch_project_views;
|
||||||
pub use project_views::{ProjectViews, ProjectViewsField, ProjectViewsFilters};
|
pub use project_views::{ProjectViews, ProjectViewsField, ProjectViewsFilters};
|
||||||
|
|||||||
@@ -39,13 +39,29 @@ pub enum ProjectRevenueField {
|
|||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct ProjectRevenueFilters {}
|
pub struct ProjectRevenueFilters {}
|
||||||
|
|
||||||
|
/// Whether project revenue is provisional or finalized.
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ProjectRevenueKind {
|
||||||
|
/// Provisional revenue which can change before finalization.
|
||||||
|
Estimated,
|
||||||
|
/// Finalized creator revenue.
|
||||||
|
Actual,
|
||||||
|
}
|
||||||
|
|
||||||
/// [`super::ReturnMetrics::project_revenue`].
|
/// [`super::ReturnMetrics::project_revenue`].
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, utoipa::ToSchema)]
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
||||||
pub struct ProjectRevenue {
|
pub struct ProjectRevenue {
|
||||||
/// User these metrics are for.
|
/// User these metrics are for.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub user_id: Option<UserId>,
|
pub user_id: Option<UserId>,
|
||||||
|
/// Whether this revenue is estimated or actual.
|
||||||
|
pub revenue_kind: ProjectRevenueKind,
|
||||||
/// Total revenue for this bucket.
|
/// Total revenue for this bucket.
|
||||||
|
#[serde(with = "rust_decimal::serde::float")]
|
||||||
|
#[schema(value_type = f64)]
|
||||||
pub(crate) revenue: Decimal,
|
pub(crate) revenue: Decimal,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,16 +92,25 @@ pub(crate) async fn fetch(
|
|||||||
WHEN $5 AND ($6 OR mod_id = ANY($7)) THEN user_id
|
WHEN $5 AND ($6 OR mod_id = ANY($7)) THEN user_id
|
||||||
ELSE 0
|
ELSE 0
|
||||||
END AS "user_id?",
|
END AS "user_id?",
|
||||||
|
estimated AS "estimated!",
|
||||||
SUM(amount) AS "amount_sum?"
|
SUM(amount) AS "amount_sum?"
|
||||||
FROM payouts_values
|
FROM (
|
||||||
|
SELECT user_id, mod_id, amount, created, FALSE AS estimated
|
||||||
|
FROM payouts_values
|
||||||
|
WHERE mod_id IS NOT NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT user_id, mod_id, amount, created, TRUE AS estimated
|
||||||
|
FROM payout_estimates
|
||||||
|
) creator_revenue
|
||||||
WHERE
|
WHERE
|
||||||
-- only project revenue is counted here
|
-- only project revenue is counted here
|
||||||
-- for affiliate code revenue, see `affiliate_code_revenue`
|
-- for affiliate code revenue, see `affiliate_code_revenue`
|
||||||
payouts_values.mod_id IS NOT NULL
|
creator_revenue.mod_id = ANY($4)
|
||||||
AND payouts_values.mod_id = ANY($4)
|
|
||||||
AND created >= $1
|
AND created >= $1
|
||||||
AND created < $2
|
AND created < $2
|
||||||
GROUP BY 1, 2, 3
|
GROUP BY 1, 2, 3, 4
|
||||||
"#,
|
"#,
|
||||||
req.time_range.start,
|
req.time_range.start,
|
||||||
req.time_range.end,
|
req.time_range.end,
|
||||||
@@ -126,6 +151,11 @@ pub(crate) async fn fetch(
|
|||||||
.filter(|id| bucket_by_user_id && *id != 0)
|
.filter(|id| bucket_by_user_id && *id != 0)
|
||||||
.map(DBUserId)
|
.map(DBUserId)
|
||||||
.map(UserId::from),
|
.map(UserId::from),
|
||||||
|
revenue_kind: if row.estimated {
|
||||||
|
ProjectRevenueKind::Estimated
|
||||||
|
} else {
|
||||||
|
ProjectRevenueKind::Actual
|
||||||
|
},
|
||||||
revenue,
|
revenue,
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1072,6 +1072,7 @@ mod tests {
|
|||||||
source_project: test_project_3,
|
source_project: test_project_3,
|
||||||
metrics: ProjectMetrics::Revenue(ProjectRevenue {
|
metrics: ProjectMetrics::Revenue(ProjectRevenue {
|
||||||
user_id: None,
|
user_id: None,
|
||||||
|
revenue_kind: ProjectRevenueKind::Actual,
|
||||||
revenue: Decimal::new(20000, 2),
|
revenue: Decimal::new(20000, 2),
|
||||||
}),
|
}),
|
||||||
})]),
|
})]),
|
||||||
@@ -1100,7 +1101,8 @@ mod tests {
|
|||||||
{
|
{
|
||||||
"source_project": test_project_3.to_string(),
|
"source_project": test_project_3.to_string(),
|
||||||
"metric_kind": "revenue",
|
"metric_kind": "revenue",
|
||||||
"revenue": "200.00",
|
"revenue_kind": "actual",
|
||||||
|
"revenue": 200.0,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1027,10 +1027,19 @@ pub async fn payment_methods(
|
|||||||
|
|
||||||
#[derive(Serialize, utoipa::ToSchema)]
|
#[derive(Serialize, utoipa::ToSchema)]
|
||||||
pub struct UserBalance {
|
pub struct UserBalance {
|
||||||
|
/// Finalized revenue currently available to withdraw.
|
||||||
pub available: Decimal,
|
pub available: Decimal,
|
||||||
pub withdrawn_lifetime: Decimal,
|
pub withdrawn_lifetime: Decimal,
|
||||||
pub withdrawn_ytd: Decimal,
|
pub withdrawn_ytd: Decimal,
|
||||||
|
/// Finalized revenue not yet available and provisional creator estimates.
|
||||||
|
///
|
||||||
|
/// Provisional estimates remain pending regardless of their estimated
|
||||||
|
/// availability date and may change before finalization.
|
||||||
pub pending: Decimal,
|
pub pending: Decimal,
|
||||||
|
/// Revenue grouped by its finalized or estimated availability date.
|
||||||
|
///
|
||||||
|
/// These groups may include provisional creator estimates that can change
|
||||||
|
/// before finalization.
|
||||||
pub dates: HashMap<DateTime<Utc>, Decimal>,
|
pub dates: HashMap<DateTime<Utc>, Decimal>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1114,27 +1123,46 @@ async fn get_user_balance(
|
|||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
) -> Result<UserBalance, ApiError> {
|
) -> Result<UserBalance, ApiError> {
|
||||||
let payouts = sqlx::query!(
|
let payouts = sqlx::query!(
|
||||||
"
|
r#"
|
||||||
SELECT date_available, SUM(amount) sum
|
SELECT
|
||||||
FROM payouts_values
|
date_available AS "date_available!",
|
||||||
WHERE user_id = $1
|
SUM(amount) FILTER (WHERE NOT provisional) finalized_sum,
|
||||||
|
SUM(amount) FILTER (WHERE provisional) estimate_sum
|
||||||
|
FROM (
|
||||||
|
SELECT date_available, amount, FALSE provisional
|
||||||
|
FROM payouts_values
|
||||||
|
WHERE user_id = $1
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT date_available, amount, TRUE provisional
|
||||||
|
FROM payout_estimates
|
||||||
|
WHERE user_id = $1
|
||||||
|
) payout_amounts
|
||||||
GROUP BY date_available
|
GROUP BY date_available
|
||||||
ORDER BY date_available DESC
|
ORDER BY date_available DESC
|
||||||
",
|
"#,
|
||||||
user_id.0
|
user_id.0
|
||||||
)
|
)
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await
|
.await
|
||||||
.wrap_internal_err("fetching payouts from database")?;
|
.wrap_internal_err("fetching payouts from database")?;
|
||||||
|
|
||||||
|
let now = Utc::now();
|
||||||
let available = payouts
|
let available = payouts
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| x.date_available <= Utc::now())
|
.filter(|x| x.date_available <= now)
|
||||||
.fold(Decimal::ZERO, |acc, x| acc + x.sum.unwrap_or(Decimal::ZERO));
|
.fold(Decimal::ZERO, |acc, x| {
|
||||||
let pending = payouts
|
acc + x.finalized_sum.unwrap_or(Decimal::ZERO)
|
||||||
.iter()
|
});
|
||||||
.filter(|x| x.date_available > Utc::now())
|
let pending = payouts.iter().fold(Decimal::ZERO, |acc, x| {
|
||||||
.fold(Decimal::ZERO, |acc, x| acc + x.sum.unwrap_or(Decimal::ZERO));
|
let finalized = if x.date_available > now {
|
||||||
|
x.finalized_sum.unwrap_or(Decimal::ZERO)
|
||||||
|
} else {
|
||||||
|
Decimal::ZERO
|
||||||
|
};
|
||||||
|
acc + finalized + x.estimate_sum.unwrap_or(Decimal::ZERO)
|
||||||
|
});
|
||||||
|
|
||||||
let withdrawn = sqlx::query!(
|
let withdrawn = sqlx::query!(
|
||||||
"
|
"
|
||||||
@@ -1168,7 +1196,13 @@ async fn get_user_balance(
|
|||||||
pending,
|
pending,
|
||||||
dates: payouts
|
dates: payouts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| (x.date_available, x.sum.unwrap_or(Decimal::ZERO)))
|
.map(|x| {
|
||||||
|
(
|
||||||
|
x.date_available,
|
||||||
|
x.finalized_sum.unwrap_or(Decimal::ZERO)
|
||||||
|
+ x.estimate_sum.unwrap_or(Decimal::ZERO),
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+123
-133
@@ -1,16 +1,19 @@
|
|||||||
|
use actix_http::StatusCode;
|
||||||
|
use actix_web::test;
|
||||||
use ariadne::ids::base62_impl::parse_base62;
|
use ariadne::ids::base62_impl::parse_base62;
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use common::permissions::PermissionsTest;
|
use common::permissions::PermissionsTest;
|
||||||
use common::permissions::PermissionsTestContext;
|
use common::permissions::PermissionsTestContext;
|
||||||
use common::{
|
use common::{
|
||||||
|
api_common::Api,
|
||||||
api_v3::ApiV3,
|
api_v3::ApiV3,
|
||||||
database::*,
|
database::*,
|
||||||
environment::{TestEnvironment, with_test_environment},
|
environment::{TestEnvironment, with_test_environment},
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
|
||||||
use labrinth::models::teams::ProjectPermissions;
|
use labrinth::models::teams::ProjectPermissions;
|
||||||
use labrinth::queue::payouts;
|
use labrinth::queue::payouts;
|
||||||
use rust_decimal::{Decimal, prelude::ToPrimitive};
|
use rust_decimal::Decimal;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
pub mod common;
|
pub mod common;
|
||||||
|
|
||||||
@@ -19,8 +22,6 @@ pub async fn analytics_revenue() {
|
|||||||
with_test_environment(
|
with_test_environment(
|
||||||
None,
|
None,
|
||||||
|test_env: TestEnvironment<ApiV3>| async move {
|
|test_env: TestEnvironment<ApiV3>| async move {
|
||||||
let api = &test_env.api;
|
|
||||||
|
|
||||||
let alpha_project_id =
|
let alpha_project_id =
|
||||||
test_env.dummy.project_alpha.project_id.clone();
|
test_env.dummy.project_alpha.project_id.clone();
|
||||||
|
|
||||||
@@ -71,116 +72,143 @@ pub async fn analytics_revenue() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
transaction.commit().await.unwrap();
|
transaction.commit().await.unwrap();
|
||||||
|
|
||||||
let day = 86400;
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
// Test analytics endpoint with default values
|
INSERT INTO payout_estimates (
|
||||||
// - all time points in the last 2 weeks
|
period,
|
||||||
// - 1 day resolution
|
user_id,
|
||||||
let analytics = api
|
mod_id,
|
||||||
.get_analytics_revenue_deserialized(
|
amount,
|
||||||
vec![&alpha_project_id],
|
created,
|
||||||
false,
|
date_available
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
USER_USER_PAT,
|
|
||||||
)
|
)
|
||||||
.await;
|
VALUES ('2000-01-01', $1, $2, 75, $3, $4)
|
||||||
assert_eq!(analytics.len(), 1); // 1 project
|
"#,
|
||||||
let project_analytics = &analytics[&alpha_project_id];
|
USER_USER_ID_PARSED,
|
||||||
assert_eq!(project_analytics.len(), 8); // 1 days cut off, and 2 points take place on the same day. note that the day exactly 14 days ago is included
|
project_id,
|
||||||
// sorted_by_key, values in the order of smallest to largest key
|
money_time_pairs[3].1,
|
||||||
let (sorted_keys, sorted_by_key): (Vec<i64>, Vec<Decimal>) =
|
Utc::now() + Duration::days(60),
|
||||||
project_analytics
|
)
|
||||||
.iter()
|
.execute(&pool)
|
||||||
.sorted_by_key(|(k, _)| *k)
|
.await
|
||||||
.rev()
|
.unwrap();
|
||||||
.unzip();
|
|
||||||
assert_eq!(
|
|
||||||
vec![100.1, 101.0, 200.0, 311.0, 400.0, 526.0, 633.0, 800.0],
|
|
||||||
to_f64_vec_rounded_up(sorted_by_key)
|
|
||||||
);
|
|
||||||
// Ensure that the keys are in multiples of 1 day
|
|
||||||
for k in sorted_keys {
|
|
||||||
assert_eq!(k % day, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test analytics with last 900 days to include all data
|
let modern_request = test::TestRequest::post()
|
||||||
// keep resolution at default
|
.uri("/v3/analytics")
|
||||||
let analytics = api
|
.append_header(("Authorization", USER_USER_PAT.unwrap()))
|
||||||
.get_analytics_revenue_deserialized(
|
.set_json(json!({
|
||||||
vec![&alpha_project_id],
|
"time_range": {
|
||||||
false,
|
"start": Utc::now() - Duration::days(801),
|
||||||
Some(Utc::now() - Duration::days(801)),
|
"end": Utc::now() + Duration::days(1),
|
||||||
None,
|
"resolution": { "slices": 802 }
|
||||||
None,
|
},
|
||||||
USER_USER_PAT,
|
"return_metrics": {
|
||||||
)
|
"project_revenue": {
|
||||||
.await;
|
"bucket_by": [],
|
||||||
let project_analytics = &analytics[&alpha_project_id];
|
"filter_by": {}
|
||||||
assert_eq!(project_analytics.len(), 9); // and 2 points take place on the same day
|
}
|
||||||
let (sorted_keys, sorted_by_key): (Vec<i64>, Vec<Decimal>) =
|
},
|
||||||
project_analytics
|
"project_ids": [alpha_project_id]
|
||||||
.iter()
|
}))
|
||||||
.sorted_by_key(|(k, _)| *k)
|
.to_request();
|
||||||
.rev()
|
let modern_response = test_env.call(modern_request).await;
|
||||||
.unzip();
|
assert_status!(&modern_response, StatusCode::OK);
|
||||||
assert_eq!(
|
let modern_analytics: serde_json::Value =
|
||||||
vec![
|
test::read_body_json(modern_response).await;
|
||||||
100.1, 101.0, 200.0, 311.0, 400.0, 526.0, 633.0, 800.0,
|
let revenue_metrics = modern_analytics["metrics"]
|
||||||
800.0
|
.as_array()
|
||||||
],
|
.unwrap()
|
||||||
to_f64_vec_rounded_up(sorted_by_key)
|
.iter()
|
||||||
);
|
.flat_map(|slice| slice.as_array().unwrap())
|
||||||
for k in sorted_keys {
|
.filter(|metric| metric["metric_kind"] == "revenue")
|
||||||
assert_eq!(k % day, 0);
|
.collect::<Vec<_>>();
|
||||||
}
|
|
||||||
|
let actual_revenue = revenue_metrics
|
||||||
|
.iter()
|
||||||
|
.filter(|metric| metric["revenue_kind"] == "actual")
|
||||||
|
.map(|metric| metric["revenue"].as_f64().unwrap())
|
||||||
|
.sum::<f64>();
|
||||||
|
let estimated_revenue = revenue_metrics
|
||||||
|
.iter()
|
||||||
|
.filter(|metric| metric["revenue_kind"] == "estimated")
|
||||||
|
.map(|metric| metric["revenue"].as_f64().unwrap())
|
||||||
|
.sum::<f64>();
|
||||||
|
|
||||||
|
assert!((actual_revenue - 3871.1).abs() < 1e-9);
|
||||||
|
assert_eq!(estimated_revenue, 75.0);
|
||||||
|
|
||||||
|
let shared_time_slice = modern_analytics["metrics"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|slice| slice.as_array().unwrap())
|
||||||
|
.find(|slice| {
|
||||||
|
slice
|
||||||
|
.iter()
|
||||||
|
.any(|metric| metric["revenue_kind"] == "actual")
|
||||||
|
&& slice
|
||||||
|
.iter()
|
||||||
|
.any(|metric| metric["revenue_kind"] == "estimated")
|
||||||
|
});
|
||||||
|
assert!(shared_time_slice.is_some());
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_f64_rounded_up(d: Decimal) -> f64 {
|
|
||||||
d.round_dp_with_strategy(
|
|
||||||
1,
|
|
||||||
rust_decimal::RoundingStrategy::MidpointAwayFromZero,
|
|
||||||
)
|
|
||||||
.to_f64()
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_f64_vec_rounded_up(d: Vec<Decimal>) -> Vec<f64> {
|
|
||||||
d.into_iter().map(to_f64_rounded_up).collect_vec()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
pub async fn permissions_analytics_revenue() {
|
pub async fn permissions_analytics_revenue() {
|
||||||
with_test_environment(
|
with_test_environment(
|
||||||
None,
|
None,
|
||||||
|test_env: TestEnvironment<ApiV3>| async move {
|
|test_env: TestEnvironment<ApiV3>| async move {
|
||||||
let alpha_project_id =
|
|
||||||
test_env.dummy.project_alpha.project_id.clone();
|
|
||||||
let alpha_version_id =
|
|
||||||
test_env.dummy.project_alpha.version_id.clone();
|
|
||||||
let alpha_team_id = test_env.dummy.project_alpha.team_id.clone();
|
|
||||||
|
|
||||||
let api = &test_env.api;
|
let api = &test_env.api;
|
||||||
|
let pool = &test_env.db.pool;
|
||||||
|
|
||||||
let view_analytics = ProjectPermissions::VIEW_ANALYTICS;
|
let view_analytics = ProjectPermissions::VIEW_ANALYTICS;
|
||||||
|
|
||||||
// first, do check with a project
|
// first, do check with a project
|
||||||
let req_gen = |ctx: PermissionsTestContext| async move {
|
let req_gen = |ctx: PermissionsTestContext| async move {
|
||||||
let project_id = ctx.project_id.unwrap();
|
let project_id = ctx.project_id.unwrap();
|
||||||
let ids_or_slugs = vec![project_id.as_str()];
|
let project_id_i64 = parse_base62(&project_id).unwrap() as i64;
|
||||||
api.get_analytics_revenue(
|
sqlx::query!(
|
||||||
ids_or_slugs,
|
r#"
|
||||||
false,
|
INSERT INTO payout_estimates (
|
||||||
None,
|
period,
|
||||||
None,
|
user_id,
|
||||||
Some(5),
|
mod_id,
|
||||||
ctx.test_pat.as_deref(),
|
amount,
|
||||||
|
created,
|
||||||
|
date_available
|
||||||
|
)
|
||||||
|
VALUES ('2000-01-01', $1, $2, 1, NOW(), NOW())
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
"#,
|
||||||
|
USER_USER_ID_PARSED,
|
||||||
|
project_id_i64,
|
||||||
)
|
)
|
||||||
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut request = test::TestRequest::post()
|
||||||
|
.uri("/v3/analytics")
|
||||||
|
.set_json(json!({
|
||||||
|
"time_range": {
|
||||||
|
"start": Utc::now() - Duration::days(1),
|
||||||
|
"end": Utc::now() + Duration::days(1),
|
||||||
|
"resolution": { "slices": 5 }
|
||||||
|
},
|
||||||
|
"return_metrics": {
|
||||||
|
"project_revenue": {
|
||||||
|
"bucket_by": [],
|
||||||
|
"filter_by": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"project_ids": [project_id]
|
||||||
|
}));
|
||||||
|
if let Some(pat) = ctx.test_pat {
|
||||||
|
request = request.append_header(("Authorization", pat));
|
||||||
|
}
|
||||||
|
api.call(request.to_request()).await
|
||||||
};
|
};
|
||||||
|
|
||||||
PermissionsTest::new(&test_env)
|
PermissionsTest::new(&test_env)
|
||||||
@@ -188,12 +216,12 @@ pub async fn permissions_analytics_revenue() {
|
|||||||
.with_200_json_checks(
|
.with_200_json_checks(
|
||||||
// On failure, should have 0 projects returned
|
// On failure, should have 0 projects returned
|
||||||
|value: &serde_json::Value| {
|
|value: &serde_json::Value| {
|
||||||
let value = value.as_object().unwrap();
|
let value = value["projects"].as_object().unwrap();
|
||||||
assert_eq!(value.len(), 0);
|
assert_eq!(value.len(), 0);
|
||||||
},
|
},
|
||||||
// On success, should have 1 project returned
|
// On success, should have 1 project returned
|
||||||
|value: &serde_json::Value| {
|
|value: &serde_json::Value| {
|
||||||
let value = value.as_object().unwrap();
|
let value = value["projects"].as_object().unwrap();
|
||||||
assert_eq!(value.len(), 1);
|
assert_eq!(value.len(), 1);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -201,44 +229,6 @@ pub async fn permissions_analytics_revenue() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Now with a version
|
|
||||||
// Need to use alpha
|
|
||||||
let req_gen = |ctx: PermissionsTestContext| {
|
|
||||||
let alpha_version_id = alpha_version_id.clone();
|
|
||||||
async move {
|
|
||||||
let ids_or_slugs = vec![alpha_version_id.as_str()];
|
|
||||||
api.get_analytics_revenue(
|
|
||||||
ids_or_slugs,
|
|
||||||
true,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
Some(5),
|
|
||||||
ctx.test_pat.as_deref(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
PermissionsTest::new(&test_env)
|
|
||||||
.with_failure_codes(vec![200, 401])
|
|
||||||
.with_existing_project(&alpha_project_id, &alpha_team_id)
|
|
||||||
.with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true)
|
|
||||||
.with_200_json_checks(
|
|
||||||
// On failure, should have 0 versions returned
|
|
||||||
|value: &serde_json::Value| {
|
|
||||||
let value = value.as_object().unwrap();
|
|
||||||
assert_eq!(value.len(), 0);
|
|
||||||
},
|
|
||||||
// On success, should have 1 versions returned
|
|
||||||
|value: &serde_json::Value| {
|
|
||||||
let value = value.as_object().unwrap();
|
|
||||||
assert_eq!(value.len(), 0);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.simple_project_permissions_test(view_analytics, req_gen)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Cleanup test db
|
// Cleanup test db
|
||||||
test_env.cleanup().await;
|
test_env.cleanup().await;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
use actix_http::StatusCode;
|
||||||
|
use actix_web::test;
|
||||||
|
use ariadne::ids::base62_impl::parse_base62;
|
||||||
|
use chrono::{DateTime, Duration, NaiveDate, TimeZone, Utc};
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use common::{
|
||||||
|
api_v3::ApiV3,
|
||||||
|
database::{USER_USER_ID_PARSED, USER_USER_PAT},
|
||||||
|
environment::{TestEnvironment, with_test_environment},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod common;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct TestBalance {
|
||||||
|
available: Decimal,
|
||||||
|
withdrawn_lifetime: Decimal,
|
||||||
|
withdrawn_ytd: Decimal,
|
||||||
|
pending: Decimal,
|
||||||
|
dates: std::collections::HashMap<DateTime<Utc>, Decimal>,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_balance(test_env: &TestEnvironment<ApiV3>) -> TestBalance {
|
||||||
|
let request = test::TestRequest::get()
|
||||||
|
.uri("/v3/payout/balance")
|
||||||
|
.append_header(("Authorization", USER_USER_PAT.unwrap()))
|
||||||
|
.to_request();
|
||||||
|
let response = test_env.call(request).await;
|
||||||
|
assert_status!(&response, StatusCode::OK);
|
||||||
|
test::read_body_json(response).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn provisional_estimates_only_affect_pending_balance() {
|
||||||
|
with_test_environment(
|
||||||
|
None,
|
||||||
|
|test_env: TestEnvironment<ApiV3>| async move {
|
||||||
|
let pool = &test_env.db.pool;
|
||||||
|
let project_id =
|
||||||
|
parse_base62(&test_env.dummy.project_alpha.project_id).unwrap()
|
||||||
|
as i64;
|
||||||
|
let now = Utc.timestamp_opt(Utc::now().timestamp(), 0).unwrap();
|
||||||
|
let finalized_available_at = now - Duration::days(30);
|
||||||
|
let estimate_past_available_at = now - Duration::days(20);
|
||||||
|
let shared_future_available_at = now + Duration::days(20);
|
||||||
|
let initial_balance = get_balance(&test_env).await;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO payouts_values (
|
||||||
|
user_id,
|
||||||
|
mod_id,
|
||||||
|
amount,
|
||||||
|
created,
|
||||||
|
date_available
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
($1, $2, 100, $3, $4),
|
||||||
|
($1, $2, 20, $5, $6)
|
||||||
|
"#,
|
||||||
|
USER_USER_ID_PARSED,
|
||||||
|
project_id,
|
||||||
|
now - Duration::days(60),
|
||||||
|
finalized_available_at,
|
||||||
|
now - Duration::days(10),
|
||||||
|
shared_future_available_at,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO payout_estimates (
|
||||||
|
period,
|
||||||
|
user_id,
|
||||||
|
mod_id,
|
||||||
|
amount,
|
||||||
|
created,
|
||||||
|
date_available
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
($1, $2, $3, 30, $4, $5),
|
||||||
|
($6, $2, $3, 40, $7, $8)
|
||||||
|
"#,
|
||||||
|
NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(),
|
||||||
|
USER_USER_ID_PARSED,
|
||||||
|
project_id,
|
||||||
|
now - Duration::days(50),
|
||||||
|
estimate_past_available_at,
|
||||||
|
NaiveDate::from_ymd_opt(2000, 2, 1).unwrap(),
|
||||||
|
now - Duration::days(10),
|
||||||
|
shared_future_available_at,
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let balance = get_balance(&test_env).await;
|
||||||
|
assert_eq!(
|
||||||
|
balance.available,
|
||||||
|
initial_balance.available + Decimal::from(100),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
balance.pending,
|
||||||
|
initial_balance.pending + Decimal::from(90),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
balance.withdrawn_lifetime,
|
||||||
|
initial_balance.withdrawn_lifetime,
|
||||||
|
);
|
||||||
|
assert_eq!(balance.withdrawn_ytd, initial_balance.withdrawn_ytd);
|
||||||
|
assert_eq!(
|
||||||
|
balance.dates[&finalized_available_at]
|
||||||
|
- initial_balance
|
||||||
|
.dates
|
||||||
|
.get(&finalized_available_at)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
Decimal::from(100),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
balance.dates[&estimate_past_available_at]
|
||||||
|
- initial_balance
|
||||||
|
.dates
|
||||||
|
.get(&estimate_past_available_at)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
Decimal::from(30),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
balance.dates[&shared_future_available_at]
|
||||||
|
- initial_balance
|
||||||
|
.dates
|
||||||
|
.get(&shared_future_available_at)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
Decimal::from(60),
|
||||||
|
);
|
||||||
|
|
||||||
|
let history_request = test::TestRequest::get()
|
||||||
|
.uri("/v3/payout/history")
|
||||||
|
.append_header(("Authorization", USER_USER_PAT.unwrap()))
|
||||||
|
.to_request();
|
||||||
|
let history_response = test_env.call(history_request).await;
|
||||||
|
assert_status!(&history_response, StatusCode::OK);
|
||||||
|
let history: Vec<serde_json::Value> =
|
||||||
|
test::read_body_json(history_response).await;
|
||||||
|
let available_entries = history
|
||||||
|
.iter()
|
||||||
|
.filter(|item| item["type"] == "payout_available")
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert!(available_entries.iter().any(|item| {
|
||||||
|
serde_json::from_value::<DateTime<Utc>>(item["created"].clone())
|
||||||
|
.unwrap()
|
||||||
|
== finalized_available_at
|
||||||
|
&& serde_json::from_value::<Decimal>(item["amount"].clone())
|
||||||
|
.unwrap()
|
||||||
|
== Decimal::from(100)
|
||||||
|
}));
|
||||||
|
assert!(available_entries.iter().all(|item| {
|
||||||
|
serde_json::from_value::<DateTime<Utc>>(item["created"].clone())
|
||||||
|
.unwrap()
|
||||||
|
!= estimate_past_available_at
|
||||||
|
}));
|
||||||
|
|
||||||
|
let affiliate_request = test::TestRequest::post()
|
||||||
|
.uri("/v3/analytics")
|
||||||
|
.append_header(("Authorization", USER_USER_PAT.unwrap()))
|
||||||
|
.set_json(json!({
|
||||||
|
"time_range": {
|
||||||
|
"start": now - Duration::days(365),
|
||||||
|
"end": now + Duration::days(365),
|
||||||
|
"resolution": { "slices": 2 }
|
||||||
|
},
|
||||||
|
"return_metrics": {
|
||||||
|
"affiliate_code_revenue": {
|
||||||
|
"bucket_by": [],
|
||||||
|
"filter_by": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.to_request();
|
||||||
|
let affiliate_response = test_env.call(affiliate_request).await;
|
||||||
|
assert_status!(&affiliate_response, StatusCode::OK);
|
||||||
|
let affiliate_analytics: serde_json::Value =
|
||||||
|
test::read_body_json(affiliate_response).await;
|
||||||
|
assert!(
|
||||||
|
affiliate_analytics["metrics"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.all(|slice| slice.as_array().unwrap().is_empty())
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
@@ -700,9 +700,12 @@ export namespace Labrinth {
|
|||||||
seconds: number
|
seconds: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProjectRevenueKind = 'estimated' | 'actual'
|
||||||
|
|
||||||
export type ProjectRevenue = {
|
export type ProjectRevenue = {
|
||||||
user_id?: string
|
user_id?: string
|
||||||
revenue: string
|
revenue_kind: ProjectRevenueKind
|
||||||
|
revenue: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AffiliateCodeAnalytics = {
|
export type AffiliateCodeAnalytics = {
|
||||||
@@ -723,7 +726,7 @@ export namespace Labrinth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AffiliateCodeRevenue = {
|
export type AffiliateCodeRevenue = {
|
||||||
revenue: string
|
revenue: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user