Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions backend/analytics/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
GameRecipeOutput,
)
from planning.models import PlanningCX, PlanningEmpire, PlanningEmpirePlan, PlanningPlan
from planning.services.empire_state_service import EmpireStateService
from user.models import User

from analytics.models import AppStatistic
Expand Down Expand Up @@ -113,3 +114,34 @@ def analytics_update_plan_insight_aggregates():

except Exception as exc:
log.error('exception', exc_info=exc)


@shared_task(name='analytics_bulk_materialize_empire_snapshots')
def analytics_bulk_materialize_empire_snapshots():
structlog.contextvars.bind_contextvars(
task_category='analytics_bulk_materialize_empire_snapshots',
)

log = logger.bind(name='analytics_bulk_materialize_empire_snapshots')

# find all dirty PlanningEmpire and process in chunks
dirty_empires = PlanningEmpire.objects.filter(needs_state_sync=True).iterator(chunk_size=100)

processed_count = 0
error_count = 0

for empire in dirty_empires:
try:
EmpireStateService.sync_snapshot(empire)

# clear and update flag only
empire.needs_state_sync = False
empire.save(update_fields=['needs_state_sync'])

processed_count += 1

except Exception as exc:
error_count += 1
log.error('exception', exc_info=exc)

log.info('completed', processed=processed_count, errors=error_count)
4 changes: 2 additions & 2 deletions backend/gamedata/fio/schemas/fio_webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class FIOExchangeOrderSchema(BaseModel):
company_id: str = Field(..., min_length=32, max_length=32, alias='CompanyId')
company_name: str = Field(..., max_length=200, alias='CompanyName')
company_code: str = Field(..., min_length=1, max_length=10, alias='CompanyCode')
item_count: int | None = Field(default=None, min=1, alias='ItemCount')
item_cost: float = Field(min=0.0, alias='ItemCost')
item_count: int | None = Field(default=None, ge=1, alias='ItemCount')
item_cost: float = Field(ge=0.0, alias='ItemCost')

class FIOExchangeBuyOrderSchema(FIOExchangeOrderSchema):
cx_buy_order_id: str = Field(..., min_length=32, max_length=32, alias='CXBuyOrderId')
Expand Down
3 changes: 2 additions & 1 deletion backend/planning/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ class PlanningPlanAdmin(ModelAdmin):

@admin.register(PlanningEmpire)
class PlanningEmpireAdmin(ModelAdmin):
list_display = ['uuid', 'user', 'empire_name', 'created_at', 'modified_at']
list_display = ['uuid', 'user', 'empire_name', 'created_at', 'modified_at', 'needs_state_sync']
search_fields = ['uuid', 'empire_name', 'user__username']
ordering = ['-modified_at']
list_filter = ['needs_state_sync']

inlines = [PlanningEmpirePlanInline]

Expand Down
2 changes: 1 addition & 1 deletion backend/planning/api/viewsets/empire_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def sync_state(self, request, pk=None):
serializer.is_valid(raise_exception=True)

# handle empire + relational snapshot refresh
EmpireStateService.sync_empire_state(instance, serializer.validated_data)
EmpireStateService.update_state(instance, serializer.validated_data)

# clear caches
PlanningCacheManager.delete_pattern(f'*PLANNING:{request.user.id}:*')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 6.0.4 on 2026-04-28 09:12

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('planning', '0006_planningempire_empire_state'),
]

operations = [
migrations.AddField(
model_name='planningempire',
name='needs_state_sync',
field=models.BooleanField(db_index=True, default=False),
),
]
2 changes: 2 additions & 0 deletions backend/planning/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ class PlanningEmpire(UUIDModel, ChangeTrackedModel):
empire_permits_used = models.PositiveIntegerField(validators=[MinValueValidator(1)])
empire_permits_total = models.PositiveIntegerField(validators=[MinValueValidator(2)])

# state
empire_state = models.JSONField(default=dict, blank=True)
needs_state_sync = models.BooleanField(default=False, db_index=True)

def __str__(self) -> str:
return f'{self.empire_name} ({self.uuid})'
Expand Down
26 changes: 17 additions & 9 deletions backend/planning/services/empire_state_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,31 @@

class EmpireStateService:
@staticmethod
def sync_empire_state(empire: PlanningEmpire, state_data: dict) -> None:
def update_state(empire: PlanningEmpire, state_data: dict) -> None:
"""Updates the empires state JSON dict only and sets the needs_state_sync flag to True"""

# update the json field
empire.empire_state = state_data
empire.save(update_fields=['empire_state', 'modified_at'])
empire.needs_state_sync = True
empire.save(update_fields=['empire_state', 'modified_at', 'needs_state_sync'])

# upsert / delete stale from EmpireMaterialSnapshot
@staticmethod
def sync_snapshot(empire: PlanningEmpire) -> None:
"""Performs snapshot sync into AnalyticsEmpireMaterialSnapshot object"""

state_data = empire.empire_state or {}
empire_total = state_data.get('empire_total', {})
active_tickers = []

# upsert / delete stale from EmpireMaterialSnapshot
active_tickers = []
snapshot_objs = []
for material_ticker, stats in empire_total.items():
p_raw, c_raw = stats.get('p', 0), stats.get('c', 0)

# pre-defined quantizer
quantizer = Decimal('0.000001')

for material_ticker, stats in empire_total.items():
# decimal conversion
p = Decimal(str(p_raw)).quantize(Decimal('0.000001'), rounding=ROUND_HALF_UP)
c = Decimal(str(c_raw)).quantize(Decimal('0.000001'), rounding=ROUND_HALF_UP)
p = Decimal(str(stats.get('p', 0))).quantize(quantizer, rounding=ROUND_HALF_UP)
c = Decimal(str(stats.get('c', 0))).quantize(quantizer, rounding=ROUND_HALF_UP)

d = p - c

Expand Down
Loading