diff --git a/backend/analytics/api/serializer.py b/backend/analytics/api/serializer.py index 2ae8289..bd27a30 100644 --- a/backend/analytics/api/serializer.py +++ b/backend/analytics/api/serializer.py @@ -11,3 +11,14 @@ class Meta: def get_status(self, obj) -> str: return 'success' + + +class AnalyticsMarketInsightSerializer(serializers.Serializer): + data = serializers.ListField( + child=serializers.ListField( + child=serializers.CharField(), + min_length=1, + max_length=3, + help_text='Indices: 0: ticker, 1: production, 2: consumption, 3: delta', + ) + ) diff --git a/backend/analytics/api/viewsets.py b/backend/analytics/api/viewsets.py index 6a64d6e..aa8ffb2 100644 --- a/backend/analytics/api/viewsets.py +++ b/backend/analytics/api/viewsets.py @@ -1,12 +1,12 @@ from datetime import timedelta -from analytics.api.serializer import AnalyticsPlanAggregateSerializer +from analytics.api.serializer import AnalyticsMarketInsightSerializer, AnalyticsPlanAggregateSerializer from analytics.models import AnalyticsEmpireMaterialSnapshot, AnalyticsPlanAggregate from analytics.services.analytics_cache_manager import AnalyticsCacheManager from django.db.models import Sum from django.http import Http404 from django.utils import timezone -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import OpenApiExample, extend_schema from gamedata.models.game_planet import GamePlanet from rest_framework import viewsets from rest_framework.decorators import action @@ -45,7 +45,19 @@ def fetch_data(planet_natural_id: str): class AnalyticsMarketInsightViewSet(viewsets.ViewSet): - @extend_schema(auth=[], summary='Fetch planning insights for materials') + @extend_schema( + auth=[], + summary='Fetch planning insights for materials', + responses={200: AnalyticsMarketInsightSerializer}, + examples=[ + OpenApiExample( + 'Material Insights Example', + summary='Example for positional array response', + value=[['AAR', 30.3584, 12.8593, 17.4991], ['ABH', 120.7919, 0.0, 120.7919]], + response_only=True, + ) + ], + ) @action(detail=False, methods=['get'], url_path='get-global-tracker') def get_global_materials(self, request): diff --git a/backend/gamedata/api/serializer.py b/backend/gamedata/api/serializer.py index cf918db..dde7073 100644 --- a/backend/gamedata/api/serializer.py +++ b/backend/gamedata/api/serializer.py @@ -3,12 +3,14 @@ from gamedata.models import ( GameBuilding, GameBuildingCost, + GameBuildingExpertiseChoices, GameExchangeAnalytics, GameExchangeCXPC, GameMaterial, GamePlanet, GamePlanetCOGCProgram, GamePlanetCOGCProgramChoices, + GamePlanetCurrencyCodeChoices, GamePlanetInfrastructureReport, GamePlanetResource, GameRecipe, @@ -17,6 +19,9 @@ ) from rest_framework import serializers +WORKFORCE_ORDER = ['PIONEER', 'SETTLER', 'TECHNICIAN', 'ENGINEER', 'SCIENTIST'] +WF_INDEX_MAP = {level: i for i, level in enumerate(WORKFORCE_ORDER)} + class GameMaterialSerializer(serializers.ModelSerializer): class Meta: @@ -46,6 +51,11 @@ class Meta: model = GameRecipe exclude = ['standard_recipe_name'] + @extend_schema_field( + serializers.CharField( + help_text="Composite ID formatted as 'BUILDING_TICKER#RECIPE_NAME'. Example: 'BMP#100xPE 25xPG=>20xOVE'" + ) + ) def get_recipe_id(self, obj: GameRecipe): return f'{obj.building_ticker}#{obj.recipe_name}' @@ -100,6 +110,8 @@ class GamePlanetSerializer(serializers.ModelSerializer): active_cogc_program_type = serializers.CharField(read_only=True) + production_fees = serializers.SerializerMethodField() + class Meta: model = GamePlanet fields = [ @@ -123,8 +135,55 @@ class Meta: 'resources', 'cogc_programs', 'active_cogc_program_type', + 'production_fees', ] + @extend_schema_field( + inline_serializer( + name='PlanetProductionFeeSchema', + fields={ + 'currency': serializers.ChoiceField(choices=GamePlanetCurrencyCodeChoices.choices), + 'fees': serializers.DictField( + child=serializers.ListField( + child=serializers.FloatField(), + min_length=5, + max_length=5, + help_text='[PIONEER, SETTLER, TECHNICIAN, ENGINEER, SCIENTIST]', + ), + help_text=( + 'Dictionary keys are Building Categories (GameBuildingExpertiseChoices). ' + 'Valid keys include: ' + + ', '.join([choice[0] for choice in GameBuildingExpertiseChoices.choices]) + ), + ), + }, + ) + ) + def get_production_fees(self, obj): + + # prefetched + fees = obj.production_fees.all() + if not fees: + return None + + currency = fees[0].fee_currency + + fee_map = {} + for fee in fees: + cat = fee.category + if cat not in fee_map: + # init with 0.0 for all workforces + fee_map[cat] = [0.0] * len(WORKFORCE_ORDER) + + try: + # overwrite with the values from data + idx = WF_INDEX_MAP[fee.workforce_level] + fee_map[cat][idx] = fee.fee_amount + except KeyError: + continue + + return {'currency': currency, 'fees': fee_map} + class PlanetIdsSerializer(serializers.ListSerializer): child = serializers.CharField(min_length=7, max_length=7) diff --git a/backend/gamedata/models/__init__.py b/backend/gamedata/models/__init__.py index 3f53247..f291773 100644 --- a/backend/gamedata/models/__init__.py +++ b/backend/gamedata/models/__init__.py @@ -1,4 +1,4 @@ -from .game_building import GameBuilding, GameBuildingCost +from .game_building import GameBuilding, GameBuildingCost, GameBuildingExpertiseChoices from .game_exchange import GameExchange, GameExchangeAnalytics, GameExchangeCXPC from .game_material import GameMaterial from .game_planet import ( @@ -11,6 +11,7 @@ GamePlanetProductionFee, GamePlanetResource, GamePlanetResourceTypeChoices, + GamePlanetCurrencyCodeChoices, queryset_gameplanet, ) from .game_playerdata import GameFIOPlayerData diff --git a/backend/gamedata/models/game_planet.py b/backend/gamedata/models/game_planet.py index 0d0949f..c9d70b6 100644 --- a/backend/gamedata/models/game_planet.py +++ b/backend/gamedata/models/game_planet.py @@ -27,7 +27,7 @@ def queryset_gameplanet() -> QuerySet: .values('program_type')[:1] ) - return GamePlanet.objects.prefetch_related('cogc_programs', 'resources').annotate( + return GamePlanet.objects.prefetch_related('cogc_programs', 'resources', 'production_fees').annotate( active_cogc_program_type=Subquery(active_program_sub) )