feat: Phase 5 — events system complete
CI / Build, lint & test (push) Successful in 2m5s

Adds a data-driven event system (#47 schema, #48 activation, #49 TOTW):

- EventDefinition JSON schema (event_type, effects, date range, is_manual)
- data/events/totw_week1.json — TOTW event active by date range (2024–2030),
  injects 5 TOTW cards into the market as [EVENT] listings
- data/events/seasonal_events.json — Spring Festival and Icon Weekend,
  manual-activation-only example events
- migrations/0004_events.sql — events override table (NULL/1/0 per event)
- services/event — load_event_definitions, compute_is_active (date range +
  manual override), get_active_events, get_all_with_status, activate/deactivate
- routes/events — GET /events, GET /events/:id, POST .../activate, .../deactivate
- services/market::refresh_npc_listings now accepts event_defs, injects bonus
  market cards from active events at 2× premium price
- 5 new integration tests (events list, single, activate/deactivate cycle,
  404 on unknown, market injection verification); 19/19 passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 16:02:45 -07:00
parent f6dd41fd41
commit 1ef2c436ab
13 changed files with 522 additions and 21 deletions
+23 -5
View File
@@ -2,13 +2,15 @@ use crate::middleware::{limit_concurrency, MakeRequestUuid};
use crate::{
config::Config,
db::Pool,
models::event::EventDefinition,
models::objective::{ObjectiveDefinition, ObjectiveType},
models::pack::PackDefinition,
models::sbc::SbcDefinition,
routes,
services::{
card_db::CardDb, market, objective::load_objective_definitions,
pack::load_pack_definitions, sbc::load_sbc_definitions,
card_db::CardDb, event::load_event_definitions, market,
objective::load_objective_definitions, pack::load_pack_definitions,
sbc::load_sbc_definitions,
},
};
use anyhow::Result;
@@ -32,6 +34,7 @@ pub struct AppState {
pub pack_defs: Arc<Vec<PackDefinition>>,
pub obj_defs: Arc<Vec<ObjectiveDefinition>>,
pub sbc_defs: Arc<Vec<SbcDefinition>>,
pub event_defs: Arc<Vec<EventDefinition>>,
}
pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
@@ -39,13 +42,15 @@ pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
let pack_defs = Arc::new(load_pack_definitions(&cfg.data_dir)?);
let obj_defs = Arc::new(load_objective_definitions(&cfg.data_dir)?);
let sbc_defs = Arc::new(load_sbc_definitions(&cfg.data_dir)?);
let event_defs = Arc::new(load_event_definitions(&cfg.data_dir)?);
tracing::info!(
"Loaded {} cards, {} packs, {} objectives, {} SBCs",
"Loaded {} cards, {} packs, {} objectives, {} SBCs, {} events",
card_db.cards.len(),
pack_defs.len(),
obj_defs.len(),
sbc_defs.len(),
event_defs.len(),
);
let state = AppState {
@@ -54,19 +59,22 @@ pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
pack_defs,
obj_defs: obj_defs.clone(),
sbc_defs,
event_defs: event_defs.clone(),
};
// Background task: refresh NPC market at startup and every 24 hours
{
let pool = pool.clone();
let card_db = card_db.clone();
let event_defs_bg = event_defs.clone();
tokio::spawn(async move {
if let Err(e) = market::refresh_npc_listings(&pool, &card_db).await {
if let Err(e) = market::refresh_npc_listings(&pool, &card_db, &event_defs_bg).await {
tracing::warn!("initial market refresh failed: {e}");
}
loop {
tokio::time::sleep(tokio::time::Duration::from_secs(86_400)).await;
if let Err(e) = market::refresh_npc_listings(&pool, &card_db).await {
if let Err(e) = market::refresh_npc_listings(&pool, &card_db, &event_defs_bg).await
{
tracing::warn!("market refresh failed: {e}");
}
}
@@ -148,6 +156,16 @@ pub async fn build(pool: Pool, cfg: Config) -> Result<Router> {
.route("/settings", get(routes::settings::get_settings))
.route("/settings", put(routes::settings::put_settings))
.route("/draft/squad", get(routes::draft::get_draft_squad))
.route("/events", get(routes::events::get_events))
.route("/events/:event_id", get(routes::events::get_event))
.route(
"/events/:event_id/activate",
post(routes::events::post_activate_event),
)
.route(
"/events/:event_id/deactivate",
post(routes::events::post_deactivate_event),
)
// Layers run outermost-first (last added = outermost).
// CORS → body limit → concurrency limit → request ID → trace
.layer(PropagateRequestIdLayer::x_request_id())