diff --git a/packages/discovery-provider/src/api/v1/models/users.py b/packages/discovery-provider/src/api/v1/models/users.py index a1e11be5af0..f1336022d47 100644 --- a/packages/discovery-provider/src/api/v1/models/users.py +++ b/packages/discovery-provider/src/api/v1/models/users.py @@ -171,3 +171,12 @@ "access": StringEnumToLower(required=True), }, ) + +sales_aggregate = ns.model( + "sales_aggregate", + { + "content_type": StringEnumToLower(required=True, discriminator=True), + "content_id": fields.String(required=True), + "purchase_count": fields.Integer(required=True), + }, +) diff --git a/packages/discovery-provider/src/api/v1/users.py b/packages/discovery-provider/src/api/v1/users.py index 04696ee50a4..526dec5cb17 100644 --- a/packages/discovery-provider/src/api/v1/users.py +++ b/packages/discovery-provider/src/api/v1/users.py @@ -75,6 +75,7 @@ decoded_user_token, encoded_user_id, purchase, + sales_aggregate, user_model, user_model_full, user_subscribers, @@ -118,6 +119,7 @@ from src.queries.get_related_artists import get_related_artists from src.queries.get_remixers import GetRemixersArgs, get_remixers, get_remixers_count from src.queries.get_repost_feed_for_user import get_repost_feed_for_user +from src.queries.get_sales_aggregate import GetSalesAggregateArgs, get_sales_aggregate from src.queries.get_saves import get_saves from src.queries.get_subscribers import ( get_subscribers_for_user, @@ -2339,6 +2341,37 @@ def get(self, id, authed_user_id): return success_response(count) +sales_aggregate_parser = pagination_with_current_user_parser.copy() + +sales_aggregate_response = make_response( + "sales_aggregate_response", ns, fields.List(fields.Nested(sales_aggregate)) +) + + +@ns.route("//sales/aggregate") +class SalesAggregate(Resource): + @ns.doc( + id="Get Sales Aggregate", + description="Gets the aggregated sales data for the user", + params={"id": "A User ID"}, + ) + @ns.expect(sales_aggregate_parser) + @auth_middleware(sales_aggregate_parser) + def get(self, id, authed_user_id): + decoded_id = decode_with_abort(id, ns) + check_authorized(decoded_id, authed_user_id) + args = sales_aggregate_parser.parse_args() + limit = get_default_max(args.get("limit"), 10, 100) + offset = get_default_max(args.get("offset"), 0) + args = GetSalesAggregateArgs( + seller_user_id=decoded_id, + limit=limit, + offset=offset, + ) + sales_aggregate = get_sales_aggregate(args) + return success_response(list(sales_aggregate)) + + purchases_download_parser = current_user_parser.copy() diff --git a/packages/discovery-provider/src/queries/get_sales_aggregate.py b/packages/discovery-provider/src/queries/get_sales_aggregate.py new file mode 100644 index 00000000000..dcc5defa8a2 --- /dev/null +++ b/packages/discovery-provider/src/queries/get_sales_aggregate.py @@ -0,0 +1,41 @@ +from typing import Optional, TypedDict + +from sqlalchemy import func + +from src.models.users.usdc_purchase import USDCPurchase +from src.queries.query_helpers import add_query_pagination +from src.utils.db_session import get_db_read_replica + + +class GetSalesAggregateArgs(TypedDict): + seller_user_id: Optional[int] + limit: int + offset: int + + +def _get_sales_aggregate(session, args: GetSalesAggregateArgs): + query = ( + session.query( + USDCPurchase.content_id, + USDCPurchase.content_type, + func.count(USDCPurchase.buyer_user_id).label("purchase_count"), + ) + .filter(USDCPurchase.seller_user_id == args.get("seller_user_id")) + .group_by(USDCPurchase.content_id, USDCPurchase.content_type) + ) + + return query + + +def get_sales_aggregate(args: GetSalesAggregateArgs): + """Aggregates USDCPurchase count by content_id and content_type for a given seller_user_id.""" + db = get_db_read_replica() + with db.scoped_session() as session: + query = _get_sales_aggregate(session, args) + limit = args.get("limit") + offset = args.get("offset") + + results = add_query_pagination(query, limit, offset).all() + sales_aggregate_list = [row._asdict() for row in results] + + return sales_aggregate_list diff --git a/packages/libs/src/sdk/api/generated/default/apis/UsersApi.ts b/packages/libs/src/sdk/api/generated/default/apis/UsersApi.ts index 917d8bc32be..285ae7c88e5 100644 --- a/packages/libs/src/sdk/api/generated/default/apis/UsersApi.ts +++ b/packages/libs/src/sdk/api/generated/default/apis/UsersApi.ts @@ -184,6 +184,15 @@ export interface GetRepostsRequest { userId?: string; } +export interface GetSalesAggregateRequest { + id: string; + offset?: number; + limit?: number; + userId?: string; + encodedDataMessage?: string; + encodedDataSignature?: string; +} + export interface GetSubscribersRequest { id: string; offset?: number; @@ -890,6 +899,56 @@ export class UsersApi extends runtime.BaseAPI { return await response.value(); } + /** + * @hidden + * Gets the aggregated sales data for the user + */ + async getSalesAggregateRaw(params: GetSalesAggregateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (params.id === null || params.id === undefined) { + throw new runtime.RequiredError('id','Required parameter params.id was null or undefined when calling getSalesAggregate.'); + } + + const queryParameters: any = {}; + + if (params.offset !== undefined) { + queryParameters['offset'] = params.offset; + } + + if (params.limit !== undefined) { + queryParameters['limit'] = params.limit; + } + + if (params.userId !== undefined) { + queryParameters['user_id'] = params.userId; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + if (params.encodedDataMessage !== undefined && params.encodedDataMessage !== null) { + headerParameters['Encoded-Data-Message'] = String(params.encodedDataMessage); + } + + if (params.encodedDataSignature !== undefined && params.encodedDataSignature !== null) { + headerParameters['Encoded-Data-Signature'] = String(params.encodedDataSignature); + } + + const response = await this.request({ + path: `/users/{id}/sales/aggregate`.replace(`{${"id"}}`, encodeURIComponent(String(params.id))), + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + return new runtime.VoidApiResponse(response); + } + + /** + * Gets the aggregated sales data for the user + */ + async getSalesAggregate(params: GetSalesAggregateRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + await this.getSalesAggregateRaw(params, initOverrides); + } + /** * @hidden * All users that subscribe to the provided user