feat: card-of-the-week endpoint for automated content pipeline #212
No reviewers
Labels
No Label
ai-changes-requested
ai-failed
ai-merged
ai-pr-opened
ai-reviewed
ai-reviewing
ai-working
autonomous
bug
enhancement
evolution
performance
phase-0
phase-1a
phase-1b
phase-1c
phase-1d
security
size:M
size:S
tech-debt
todo
type:feature
type:stability
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: cal/paper-dynasty-database#212
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "autonomous/feat-card-of-the-week-endpoint"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Autonomous Pipeline Finding
Source: growth-po nightly sweep (2026-04-10)
Roadmap: Phase 2.6c
Category: feature / content pipeline
What
New endpoint
GET /api/v2/cards/featured/card-of-the-weekreturning the highest-rated eligible card from the past 7 days in a single response shape ready for Discord embed consumption.Why
Lowest-friction entry into the automated content pipeline — proves the pull-model content pattern before committing to webhooks (see roadmap 2.6a). Keeps Discord active between pack drops, showcases the new refractor art, and creates a reusable template for weekly stat leaders (2.6b).
Design Notes
Pack.open_time(when the pack was opened = when the card entered a team's collection). TheCardmodel has nocreated_atcolumn; this is the correct semantic proxy.Card.value. Tiebreak:value DESC,open_time DESC,card.id DESC— fully deterministic.Team.is_ai != 0); opt-in via?include_ai=true.?days=7(1-90) and?include_ai=false.Test Plan
python -m pytest tests/test_card_of_the_week.py -v)curl pddev.manticorum.com:813/api/v2/cards/featured/card-of-the-weekon dev (reviewer)🤖 Generated via Paper Dynasty autonomous pipeline (2026-04-10)
AI Code Review
Files Reviewed
app/routers_v2/featured.py(added)app/main.py(modified — import + router registration)tests/test_card_of_the_week.py(added)Findings
Correctness
No issues found.
.switch(Card)and.switch(Player)are used correctly to navigate back up the join tree.(Team.is_ai == 0) | (Team.is_ai.is_null(True))correctly keeps human teams. The schema confirmsis_ai = IntegerField(null=True)where1= AI and0 / NULL= human. Logic matches the docstring.Pack.open_timeisnull=Truein the schema. Cards from unopened packs (open_time IS NULL) are silently excluded by the>= cutoffWHERE clause — NULL comparisons evaluate false in SQL. This is the correct semantic: only opened packs are meaningful for this endpoint.value DESC, open_time DESC, card.id DESCis fully deterministic; no random element.cards.pyand other v2 routers). Only mutating endpoints require the token. Consistent.Security
No issues found. No user-supplied data lands in raw queries;
daysis an integer bounded to[1, 90]via FastAPIQuery(ge=1, le=90). No secrets or credentials introduced.Style & Conventions
Two minor cosmetic items that do not block merge:
Optional[str]without= None(featured.pyline 53): In Pydantic v1 (confirmedpydantic==1.10.21),Optional[str]without a default means the field is technically required at construction time. The response construction always providescard_created_at=pack_open_timeexplicitly so there is no runtime failure, but the convention isOptional[str] = Noneto signal that None is a valid value. Safe to fix in a follow-up._PACK_COUNTERis defined but never used (test_card_of_the_week.pyline ~149):_make_packdoes not increment or read_PACK_COUNTER. Dead code — harmless but could be removed.Suggestions
Cache-Control: max-age=3600). The PR description mentions "cache-friendly" and "deterministic within a calendar day" — consider adding aCache-Controlheader in a follow-up so n8n/Cloudflare can actually cache the response.datetime.utcnow()is deprecated in Python 3.12+. Pre-existing pattern across the codebase, so no action needed in this PR, but worth noting for a future cleanup pass.Test Coverage
All 6 required scenarios covered:
test_happy_path_single_eligible_card— response shape + field valuestest_empty_window_returns_404— 404 contracttest_ai_team_excluded_by_default— default exclusiontest_include_ai_true_includes_ai_team_card— opt-in inclusiontest_tiebreak_highest_rating_wins— primary sorttest_tiebreak_same_rating_newest_pack_wins— secondary sort (determinism)Factory data pattern follows CLAUDE.md conventions. Shared-memory SQLite + per-test table teardown provides proper isolation.
Verdict: APPROVED
Implementation is correct, joins and filters match the schema, AI exclusion logic is sound, tiebreak is deterministic, tests cover the full contract. The two style notes above are cosmetic and do not warrant blocking merge.
Note: posted as COMMENT state due to single-user repo restriction (Gitea blocks self-approval). Intended verdict is APPROVED — merge via
pd-pr merge --no-approve.Automated review by Claude PR Reviewer
AI Code Review
Files Reviewed
app/main.py(modified — router wiring)app/routers_v2/featured.py(added, 127 lines)tests/test_card_of_the_week.py(added, 470 lines)Findings
Correctness
No blocking issues found.
Card → Pack → Player → Rarity,.switch(Player) → Cardset,.switch(Card) → Team— all INNER JOINs with correct.switch()resets. ✅(Team.is_ai == 0) | (Team.is_ai.is_null(True))correctly keeps human teams (is_ai=0 or NULL) and excludes AI teams (is_ai=1+). Matches the documented behavior. ✅.where(Card.player.is_null(False)),.where(Card.team.is_null(False)),.where(Card.pack.is_null(False))are redundant given the INNER JOINs, but harmless. ✅Card.value DESC, Pack.open_time DESC, Card.id DESC— fully deterministic as documented. ✅card.pack.open_timeaccess safe:Pack.open_timeisDateTimeField(null=True)on the model; theif card.pack and card.pack.open_time:guard is correct. ✅Security
days(int, ge=1, le=90) andinclude_ai(bool) are validated by FastAPI before reaching the handler. ✅Style & Conventions
from ..db_engine import ...at module top-level — follows CLAUDE.md lazy-import rule. ✅_PACK_COUNTERis defined at module level but never incremented or read — dead variable. Harmless._make_pack_type(cardset: Cardset)accepts acardsetargument but never uses it —PackTypehas nocardsetFK. Dead parameter. Harmless, but misleading.Suggestions
card_image_urlfield naming: The field is populated fromcard.player.image(Player.image, a CharField), which is the player's stock/template photo URL — not the Playwright-rendered card PNG. For Discord embeds that want to show the actual card art (including refractor shimmer), callers will need a follow-up call to/api/v2/players/{id}/...card/.../animatedor the static card endpoint. Consider renaming toplayer_image_urlin a follow-up to avoid misleading consumers, or noting the distinction in the endpoint description.Card.variantis not filtered, so refractor variant cards (variant > 0) can win card-of-the-week. Theircard_image_urlstill points to the basePlayer.image, not the refractor-rendered art. If refractor cards are expected to appear with their shimmer art, a follow-up to includeimage_urlfromBattingCard/PitchingCardwould be needed. Design call — not a blocker.?days=boundary test: The test matrix doesn't include a custom?days=14or?days=1case. The 6 documented tests are all present and pass. Low priority.Verdict: COMMENT
Functionally correct. Query logic, AI exclusion, tiebreak, and 404 handling are all sound. Tests cover the documented matrix. Two dead code nits (
_PACK_COUNTER, unusedcardsetparam in_make_pack_type) are harmless. Primary suggestion is a follow-up to clarifycard_image_urlvs the rendered card image — not a blocker for merge.Self-review restriction applies (autonomous pipeline PR, same repo owner) — no blocking issues, ready to merge.
Automated review by Claude PR Reviewer
TODO: Let's rename this "Pull of the Week" instead of Card of the Week so we guarantee there is no confusion about what it is. Card of the Week is a bit ambiguous and could refer to statistics as well.
AI Code Review
Files Reviewed
app/main.py(modified — router import + registration)app/routers_v2/featured.py(added — new endpoint)tests/test_card_of_the_week.py(added — 6 tests)Findings
Correctness
No issues found.
Card→Pack,.switch(Card)→Player→Rarity,.switch(Player)→Cardset,.switch(Card)→Team) is correct;.switch()resets are in the right places ✅(Team.is_ai == 0) | (Team.is_ai.is_null(True))correctly matches the model (is_ai = IntegerField(null=True)where non-AI teams have0orNULL) ✅ORDER BY Card.value DESC, Pack.open_time DESC, Card.id DESCis deterministic ✅Pack.open_time >= cutoffWHERE clause correctly excludes NULLopen_timerows at the SQL level (NULL comparisons always evaluate false) ✅card.pack.open_timenull guard in response assembly is redundant but harmless ✅Card.player == Player.player_idFK join condition is correct —Playerusesplayer_idas primary key ✅Security
No issues found.
daysis boundedge=1, le=90;include_aiis a bool — no unvalidated user input reaches the query.Style & Conventions
card_image_urlfield naming: populates fromcard.player.imagewhich is the stock/template headshot URL, not a Playwright-rendered card PNG. For refractor variant cards (Card.variant > 0), the shimmer art lives inBattingCard.image_url/PitchingCard.image_url, notPlayer.image. A variant card can win COTW but its image won't show the refractor art. Recommend a follow-up to either rename toplayer_image_urlor add arendered_card_urlfield that conditionally resolves variant art._PACK_COUNTERin tests: declared at module level (_PACK_COUNTER = [0]) but never incremented or referenced. Dead variable._make_pack_type(cardset)helper: accepts acardsetparameter that is never used —PackTypehas nocardsetFK. Parameter can be removed.datetime.utcnow(): deprecated since Python 3.12. Non-blocking; consistent with existing usage elsewhere in the codebase.Suggestions
Card.variant > 0from COTW results until the image URL is wired — otherwise refractor cards appear with a plain headshot instead of their animated art.Verdict: COMMENT
Self-review restriction applies (PR author = repo owner). Code is functionally correct, follows CLAUDE.md import conventions, and is secure. No blocking issues. Ready to merge pending the
card_image_url/ variant image follow-up being tracked as a separate issue.Automated review by Claude PR Reviewer
Checkout
From your project repository, check out a new branch and test the changes.