Commit Graph

9 Commits

Author SHA1 Message Date
funman300 8819cc76a1 fix(core): keep an absent manager field distinct from an explicit removal
CI / Build, lint & test (push) Successful in 3m24s
PUT /club/manager took `owned_card_id: Option<String>`, so serde collapsed
"field absent" and "field explicitly null" into the same None, and the route
treated both as a clear. A caller that simply had nothing to say about the
manager therefore DELETED the assignment.

That is the second destructive squad-save path. WAL forensics on the staging
DB pin it to commit frame 468, squad_managers 1 row -> 0, in a transaction
touching only squad_managers and its indexes - disjoint from the player wipe
at frame 465, which touched squads/squad_players/game_entity_ext. The two
wipes came from two different writes, and only the first was guarded.

The three states are now distinct:

  {}                            leave the manager exactly as it is
  {"owned_card_id": null}       explicitly remove it (still supported)
  {"owned_card_id": "<id>"}     assign that owned card

A deliberate removal is a legitimate operation and is preserved; only the
"absent means delete" reading is gone.

set_squad_manager_for_squad now runs its two existence checks and the insert
in ONE transaction. Validating on the pool and then inserting left a window in
which the squad or the card could disappear between check and write.

Tests cover assign, reassign, idempotent re-assign, absent-is-a-no-op,
explicit-null-still-removes, unowned-manager-refused, absent-against-no-manager
not over-guarded, and that no manager write disturbs player assignments. A
malformed body is asserted to be a parser rejection, distinguishable from the
guard. With the fix reverted the absent-field test fails.
2026-08-24 19:58:54 +00:00
funman300 8b1081019f feat(core): one instance-based ownership model for every kind of owned content
CI / Build, lint & test (push) Successful in 3m21s
Core could only own players. Everything else a FUT club holds — managers, staff,
consumables, kits, badges, balls, stadiums — had no representation, so the only
way to show one to a client was to synthesise it on read. That is the failure
mode this commit exists to make impossible: read authority, write authority and
persistent ownership authority are now the same rows.

MODEL. There is deliberately NO parallel items table. A manager, a consumable, a
kit and a player are all rows in `owned_cards`, differing only by a new
game-INDEPENDENT `content_kind` (player|manager|staff|consumable|kit|badge|ball|
stadium|misc). A game adapter translates its own taxonomy — FIFA 17's
`cardsubtypeid` and resource ranges — into one of those tokens before ownership
reaches Core; no game's numerics land here. Ownership stays INSTANCE-based:
`card_id` is the definition, `id` is the instance, and two copies of one
definition remain two rows.

`quantity` is a nullable per-instance attribute, not a replacement for the
instance. The real profile settles this: its 17 consumables are instance-based
and only SOME carry a wire `amount` (observed 1,2,4,5,10,15), while two copies of
definition 5003068 exist as two distinct instances. So NULL means "not a stack"
and a positive integer is the stack size; collapsing instances into counts is
forbidden by the model.

ACTIVE DESIGNATIONS. Migration 0024's two-slot kit table becomes
`club_active_items` over the five slots that correspond exactly to the client's
recovered equipped-state vocabulary (activeBadge 100, activeHomeKit 101,
activeAwayKit 102, activeBall 103, activeStadium 104). There is no
activeLeagueLogo or activeMisc token, so those kinds correctly get no slot. The
invariants are schema-enforced rather than conventional: PK(club_id, slot) allows
at most one item per role, `owned_card_id UNIQUE` makes "the same card is both
home and away kit" unstorable, and ON DELETE CASCADE means a quick-sold or
consumed item cannot be projected back as active. 0024's trigger is preserved in
semantics — and dropped EXPLICITLY before its table, because it lives ON
`owned_cards`, so DROP TABLE would have orphaned it and broken every later
ownership transfer. It still exists because the market moves ownership by UPDATE,
which no foreign key can observe.

CONSUMABLE ACTIONS. `services/consume.rs` is one transaction primitive —
validate source ownership and kind, validate target, mutate, consume the source
exactly once, commit — guarded by `UNIQUE(profile_id, action_identity)` in
migration 0027, the same discipline as `match_completions`. It supports both
deleting the row and decrementing a stack, chosen by the caller, inside the one
transaction and the one replay guard. It deliberately contains NO category
formulas: an unreversed effect must not be invented, so callers supply the
mutation and category validation stays explicit.

`/club/kits` is replaced by slot-generic `/club/active-items`. `get_collection`
now carries `content_kind` and `quantity`, accepts a `content_kind` filter, and
— importantly — stops dropping an owned card with a missing definition silently:
the envelope reports `owned_rows`, `unresolved_items` and the offending
definition ids. That silent `filter_map` is the documented cause of a club that
looks empty while the rows are all present.

Verified against a REAL populated club, not a fixture: the production snapshot
(migration 19) is copied to a tempdir, migrated to 0024, given two kit
designations on real owned instances, then migrated to head. 1986 owned rows
survive as content_kind='player', both designations land in `club_active_items`,
no row gains a quantity, and the old table is gone. 258 tests pass, clippy clean.
2026-08-21 19:10:54 +00:00
funman300 f0550e2ae1 feat(club): persist active home/away kit assignments
CI / Build, lint & test (push) Successful in 2m50s
Kits are ownership-backed club items: the owned instance stays in the
generic owned_cards inventory and only the two active roles get their own
table. This mirrors the squad_managers precedent and keeps every
FIFA-specific resourceId/wire concern in the game adapter.

* migration 0024: club_kit_assignments(club_id, slot, owned_card_id) with a
  UNIQUE owned_card_id (one instance cannot hold both roles) and
  ON DELETE CASCADE from owned_cards so a quick-sell clears the role.
* a BEFORE UPDATE OF club_id trigger clears the designation on a market
  transfer, which moves ownership by UPDATE and so is not covered by the
  cascade.
* set_active_club_kits replaces BOTH slots in one transaction, rejects
  home == away, and validates each instance against current club ownership,
  so a half-applied or dangling designation is not representable.
* get_active_club_kits revalidates ownership on read, so a stale row can
  never surface another club's item.
* GET/PUT /club/kits expose the pair.

Tests cover restart persistence, replace/clear without duplicates, atomic
rejection of invalid references, and clearing via delete and transfer.
2026-08-21 03:17:40 +00:00
funman300 9036f5f411 feat(club): ownership-backed squad manager assignment
Add a generic, durable squad->manager assignment (migration 0023
squad_managers) so a manager persists across squad save, reload, and
server restart, backed by authoritative Core owned_cards.

- squad_managers(squad_id PK, owned_card_id, updated_at) with ON DELETE
  CASCADE on both FKs: quick-selling the manager auto-clears the
  assignment (no resurrection); one manager per squad (no duplicates).
- club::{get,set,clear}_squad_manager validate club ownership of both the
  squad and the card, and re-check ownership on read (defends against a
  stale row left by a market transfer).
- GET/PUT /club/manager routes expose the assignment; FIFA wire meaning
  stays in the adapter.

Tests: persistence across reload+restart (headline), reassignment
replace/no-duplicate, clear/no-resurrection, cascade on quick-sell,
foreign-card rejection.
2026-08-20 16:43:28 +00:00
funman300 a034e74c16 style(core): apply cargo fmt across routes, services, models, tests
CI / Build, lint & test (push) Successful in 2m19s
Pure rustfmt reflow (import grouping, array/match-arm/call-arg wrapping,
alphabetized module decls, comment realignment). No semantic change:
full-diff and `git diff -w` both confirm logic byte-identical to 271c363;
workspace builds and all 74 core tests pass. Retained pre-existing WIP
brought forward after verification.
2026-08-20 16:05:41 +00:00
funman300 8c8a4116bf wip: checkpoint multi-game core work 2026-08-07 12:03:21 -07:00
funman300 956bfe7a73 Phase 24: daily check-in system + club milestones endpoint
CI / Build, lint & test (push) Failing after 28s
- migrations/0012_daily_checkin.sql: daily_checkins table tracking streak,
  coins awarded, pack granted, timestamp per profile
- services/checkin.rs: get_status() (available, streak_day, next reward),
  claim() (idempotent same-day guard, streak logic: continue if yesterday
  or today, else reset; 7-day cycle with STREAK_COINS array, day-7 pack)
- routes/club.rs: GET /club/checkin, POST /club/checkin, GET /club/milestones
  (computed from statistics, season_history, owned_cards, sbc_submissions,
  daily_checkins tables; no new DB tables needed)
- 4 new integration tests: checkin available initially, claim awards coins,
  idempotent same-day, milestones endpoint structure (93 → 93+4=97 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 19:05:52 -07:00
funman300 a749fba93c Phase 9: division/season tracking, club customization, pack history, notifications
CI / Build, lint & test (push) Failing after 1m42s
- GET /division returns live season stats (points, record, promotion threshold)
- PUT /club allows updating club name and manager_name
- GET /packs/history returns opened packs with full card definitions
- GET /notifications dynamically surfaces completed objectives, expiring loans, season end
- Club model gains manager_name column (migration 0006 already added it)
- Pack model gains opened_cards and opened_at; pack SELECT queries updated
- 9 new integration tests — all 45 pass, clippy clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 16:50:35 -07:00
funman300 1ffe0ffa9f Initial commit: OpenFUT Core
Offline Ultimate Team backend — game-independent REST API.

- 19 API endpoints: auth, profiles, clubs, cards, packs, squads,
  objectives, SBCs, match rewards, NPC market, statistics
- Axum + SQLite + SQLx with full migrations
- Weighted pack generator, SBC validation engine
- JSON-driven mod data (cards, packs, objectives, SBCs)
- 5 integration tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 14:54:51 -07:00