Skip to content

Commit 29fe92e

Browse files
committed
(PC-32268)[API] feat: Return individual or collective statistics only if offerer is concerned.
1 parent 8e347d1 commit 29fe92e

File tree

12 files changed

+241
-50
lines changed

12 files changed

+241
-50
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1+
from .yearly_revenue import YearlyAggregatedCollectiveRevenueQuery
2+
from .yearly_revenue import YearlyAggregatedIndividualRevenueQuery
13
from .yearly_revenue import YearlyAggregatedRevenueModel
24
from .yearly_revenue import YearlyAggregatedRevenueQuery

api/src/pcapi/connectors/clickhouse/queries/yearly_revenue.py

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,30 @@
77
from pcapi.routes.serialization.offers_serialize import to_camel
88

99

10-
class Revenue(pydantic_v1.BaseModel):
11-
total: Decimal
10+
class IndividualRevenue(pydantic_v1.BaseModel):
1211
individual: Decimal
12+
13+
class Config:
14+
extra = "forbid"
15+
16+
17+
class CollectiveRevenue(pydantic_v1.BaseModel):
1318
collective: Decimal
1419

1520
class Config:
1621
extra = "forbid"
1722

1823

24+
class CollectiveAndIndividualRevenue(IndividualRevenue, CollectiveRevenue):
25+
total: Decimal
26+
27+
class Config:
28+
extra = "forbid"
29+
30+
1931
class AggregatedRevenue(pydantic_v1.BaseModel):
20-
revenue: Revenue
21-
expected_revenue: Revenue
32+
revenue: CollectiveAndIndividualRevenue | CollectiveRevenue | IndividualRevenue
33+
expected_revenue: CollectiveAndIndividualRevenue | CollectiveRevenue | IndividualRevenue
2234

2335
class Config:
2436
extra = "forbid"
@@ -33,7 +45,7 @@ class Config:
3345
alias_generator = to_camel
3446

3547

36-
class YearlyAggregatedRevenueQuery(BaseQuery[YearlyAggregatedRevenueModel]):
48+
class YearlyAggregatedRevenueQueryMixin:
3749
def _format_result(self, results: list) -> dict:
3850
return {
3951
"incomeByYear": {
@@ -45,6 +57,54 @@ def _format_result(self, results: list) -> dict:
4557
}
4658
}
4759

60+
@property
61+
def model(self) -> type[YearlyAggregatedRevenueModel]:
62+
return YearlyAggregatedRevenueModel
63+
64+
65+
class YearlyAggregatedCollectiveRevenueQuery(
66+
YearlyAggregatedRevenueQueryMixin, BaseQuery[YearlyAggregatedRevenueModel]
67+
):
68+
@property
69+
def raw_query(self) -> str:
70+
return """
71+
SELECT
72+
EXTRACT(YEAR FROM creation_year) AS year,
73+
toJSONString(map(
74+
'collective', ROUND(SUM(revenue),2),
75+
) as revenue,
76+
toJSONString(map(
77+
'collective', ROUND(SUM(expected_revenue),2),
78+
) as expected_revenue
79+
FROM analytics.yearly_aggregated_venue_collective_revenue
80+
WHERE "venue_id" in %s
81+
GROUP BY year
82+
ORDER BY year
83+
"""
84+
85+
86+
class YearlyAggregatedIndividualRevenueQuery(
87+
YearlyAggregatedRevenueQueryMixin, BaseQuery[YearlyAggregatedRevenueModel]
88+
):
89+
@property
90+
def raw_query(self) -> str:
91+
return """
92+
SELECT
93+
EXTRACT(YEAR FROM creation_year) AS year,
94+
toJSONString(map(
95+
'individual', ROUND(SUM(revenue),2),
96+
) as revenue,
97+
toJSONString(map(
98+
'individual', ROUND(SUM(expected_revenue),2),
99+
) as expected_revenue
100+
FROM analytics.yearly_aggregated_venue_individual_revenue
101+
WHERE "venue_id" in %s
102+
GROUP BY year
103+
ORDER BY year
104+
"""
105+
106+
107+
class YearlyAggregatedRevenueQuery(YearlyAggregatedRevenueQueryMixin, BaseQuery[YearlyAggregatedRevenueModel]):
48108
@property
49109
def raw_query(self) -> str:
50110
return """
@@ -65,7 +125,3 @@ def raw_query(self) -> str:
65125
GROUP BY year
66126
ORDER BY year
67127
"""
68-
69-
@property
70-
def model(self) -> type[YearlyAggregatedRevenueModel]:
71-
return YearlyAggregatedRevenueModel

api/src/pcapi/core/offers/repository.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1326,3 +1326,14 @@ def merge_products(to_keep: models.Product, to_delete: models.Product) -> models
13261326
db.session.delete(to_delete)
13271327

13281328
return to_keep
1329+
1330+
1331+
def venues_have_individual_and_collective_offers(venue_ids: list[int]) -> tuple[bool, bool]:
1332+
return (
1333+
db.session.query(offers_model.Offer.query.filter(offers_model.Offer.venueId.in_(venue_ids)).exists()).scalar(),
1334+
db.session.query(
1335+
educational_models.CollectiveOffer.query.filter(
1336+
educational_models.CollectiveOffer.venueId.in_(venue_ids)
1337+
).exists()
1338+
).scalar(),
1339+
)

api/src/pcapi/core/users/repository.py

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from dateutil.relativedelta import relativedelta
77
from flask_sqlalchemy import BaseQuery
88
import sqlalchemy as sa
9-
from sqlalchemy.orm import joinedload
9+
from sqlalchemy.dialects import postgresql
1010
from sqlalchemy.sql.functions import func
1111

1212
import pcapi.core.offerers.models as offerers_models
@@ -89,28 +89,19 @@ def has_access(user: models.User, offerer_id: int) -> bool:
8989

9090

9191
def has_access_to_venues(user: models.User, venue_ids: list[int]) -> bool:
92-
"""Return whether the user has access to the requested venues' data."""
93-
query = offerers_models.UserOfferer.query
94-
query = query.options(
95-
joinedload(offerers_models.UserOfferer).load_only(
96-
offerers_models.UserOfferer.offererId,
97-
offerers_models.UserOfferer.userId,
98-
offerers_models.UserOfferer.isValidated,
92+
"""Return whether the user has access to all the requested venues' data."""
93+
return db.session.execute(
94+
sa.select(
95+
sa.cast(postgresql.array(venue_ids), postgresql.ARRAY(postgresql.BIGINT)).contained_by(
96+
sa.func.array_agg(offerers_models.Venue.id)
97+
)
9998
)
100-
)
101-
query = query.options(
102-
joinedload(offerers_models.Venue).load_only(offerers_models.Venue.id, offerers_models.Venue.managingOffererId)
103-
)
104-
filters = [
105-
offerers_models.UserOfferer.offererId == offerers_models.Venue.managingOffererId,
106-
offerers_models.UserOfferer.userId == user.id,
107-
offerers_models.UserOfferer.isValidated,
108-
offerers_models.Venue.id.in_(venue_ids),
109-
]
110-
111-
query = db.session.query(query.filter(*filters).exists()).scalar()
112-
113-
return query
99+
.select_from(offerers_models.Venue)
100+
.join(offerers_models.Offerer)
101+
.join(offerers_models.UserOfferer)
102+
.where(offerers_models.UserOfferer.userId == user.id)
103+
.group_by(offerers_models.UserOfferer.userId)
104+
).scalar()
114105

115106

116107
def get_newly_eligible_age_18_users(since: date) -> list[models.User]:

api/src/pcapi/routes/pro/statistics.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from flask_login import login_required
33

44
from pcapi.connectors.clickhouse import queries as clickhouse_queries
5+
from pcapi.core.offers.repository import venues_have_individual_and_collective_offers
56
from pcapi.models.api_errors import ApiErrors
67
from pcapi.routes.apis import private_api
78
from pcapi.routes.serialization.statistics_serialize import StatisticsModel
@@ -26,5 +27,11 @@ def get_statistics(query: StatisticsQueryModel) -> StatisticsModel:
2627
status_code=422,
2728
)
2829
check_user_has_access_to_venues(current_user, venue_ids)
29-
result = clickhouse_queries.YearlyAggregatedRevenueQuery().execute(tuple(venue_ids))
30+
venues_have_individual, venues_have_collective = venues_have_individual_and_collective_offers(venue_ids)
31+
if not venues_have_individual:
32+
result = clickhouse_queries.YearlyAggregatedCollectiveRevenueQuery().execute(tuple(venue_ids))
33+
elif not venues_have_collective:
34+
result = clickhouse_queries.YearlyAggregatedIndividualRevenueQuery().execute(tuple(venue_ids))
35+
else:
36+
result = clickhouse_queries.YearlyAggregatedRevenueQuery().execute(tuple(venue_ids))
3037
return StatisticsModel.from_query(income_by_year=result.income_by_year)

api/tests/connectors/clickhouse/fixtures.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,27 @@ def __init__(
1414
collective: Decimal = Decimal("12.12"),
1515
expected_individual: Decimal = Decimal("13.12"),
1616
expected_collective: Decimal = Decimal("13.12"),
17+
only_collective: bool = False,
18+
only_individual: bool = False,
1719
) -> object:
1820
self.year = year
19-
self.revenue = json.dumps(
20-
{"individual": str(individual), "collective": str(collective), "total": str(individual + collective)}
21-
)
22-
self.expected_revenue = json.dumps(
23-
{
24-
"individual": str(expected_individual),
25-
"collective": str(expected_collective),
26-
"total": str(expected_individual + expected_collective),
27-
}
28-
)
21+
if only_collective:
22+
self.revenue = json.dumps({"collective": str(collective)})
23+
self.expected_revenue = json.dumps({"collective": str(expected_collective)})
24+
elif only_individual:
25+
self.revenue = json.dumps({"individual": str(individual)})
26+
self.expected_revenue = json.dumps({"individual": str(expected_individual)})
27+
else:
28+
self.revenue = json.dumps(
29+
{"individual": str(individual), "collective": str(collective), "total": str(individual + collective)}
30+
)
31+
self.expected_revenue = json.dumps(
32+
{
33+
"individual": str(expected_individual),
34+
"collective": str(expected_collective),
35+
"total": str(expected_individual + expected_collective),
36+
}
37+
)
2938

3039

3140
YEARLY_AGGREGATED_VENUE_REVENUE = [MockYearlyAggregatedRevenueQueryResult()]
@@ -35,3 +44,15 @@ def __init__(
3544
2022, Decimal("22.12"), Decimal("22.12"), Decimal("22.12"), Decimal("22.12")
3645
),
3746
]
47+
YEARLY_AGGREGATED_VENUE_REVENUE_MULTIPLE_YEARS_ONLY_COLLECTIVE = [
48+
MockYearlyAggregatedRevenueQueryResult(only_collective=True),
49+
MockYearlyAggregatedRevenueQueryResult(
50+
2022, Decimal("22.12"), Decimal("22.12"), Decimal("22.12"), Decimal("22.12"), only_collective=True
51+
),
52+
]
53+
YEARLY_AGGREGATED_VENUE_REVENUE_MULTIPLE_YEARS_ONLY_INDIVIDUAL = [
54+
MockYearlyAggregatedRevenueQueryResult(only_individual=True),
55+
MockYearlyAggregatedRevenueQueryResult(
56+
2022, Decimal("22.12"), Decimal("22.12"), Decimal("22.12"), Decimal("22.12"), only_individual=True
57+
),
58+
]

0 commit comments

Comments
 (0)