[Feature] POST /packs/buy — Purchase Packs with Coins #2

Open
opened 2026-06-26 05:05:27 +00:00 by funman300 · 0 comments
Owner

Overview

Allow the player to spend coins from their club balance to purchase a pack from the store. This is the primary coin sink in FUT.

Prerequisites

  • GET /packs/store (already implemented in Phase 15)
  • GET /club already returns the coins balance

Implementation Steps

1. Add PackPurchaseRequest to src/models/packs.rs

pub struct PackPurchaseRequest {
    pub pack_id: String,
}

2. Validate pack exists in src/services/packs.rs

pub fn get_pack_definition(pack_id: &str) -> Option<PackDefinition>

Load from data/packs.json; return 404 Not Found if the pack_id is not recognised.

3. Validate player has enough coins

  • Load current club: SELECT coins FROM clubs WHERE id = 1
  • If club.coins < pack.price → return 400 Bad Request:
{ "error": "insufficient_coins", "required": 5000, "available": 2300 }

4. Deduct coins atomically in a transaction

BEGIN;
UPDATE clubs SET coins = coins - ? WHERE id = 1 AND coins >= ?;
-- if rows_affected == 0: rollback → return 400
INSERT INTO pack_history (pack_id, coins_spent, opened_at) VALUES (?, ?, CURRENT_TIMESTAMP);
COMMIT;

Use AND coins >= ? as the atomic guard so there is no double-spend race.

5. Open the pack (reuse existing pack-open logic)

Call the existing service function that picks random cards from the pack definition and adds them to the collection.

6. Return response

{
  "pack_id": "gold_standard",
  "coins_spent": 5000,
  "coins_remaining": 17400,
  "items": [
    { "card_id": "card_123", "name": "Harry Kane", "overall": 88 }
  ]
}

7. Add pack_history migration

Create migrations/004_pack_history.sql:

CREATE TABLE IF NOT EXISTS pack_history (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    pack_id TEXT NOT NULL,
    coins_spent INTEGER NOT NULL,
    opened_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

8. Register route in src/routes/packs.rs

.route("/packs/buy", post(buy_pack))

9. Write integration test

  • Seed club with 10 000 coins
  • POST /packs/buy with valid pack_id → 200, coins deducted, items returned
  • POST /packs/buy again when balance is too low → 400 insufficient_coins

Acceptance Criteria

  • POST /packs/buy deducts coins from club balance
  • Returns 400 with insufficient_coins on low balance
  • Coin deduction is atomic (no partial state on failure)
  • Pack history recorded in pack_history table
  • Integration test covers happy path and insufficient-coins path
  • cargo clippy -- -D warnings passes
## Overview Allow the player to spend coins from their club balance to purchase a pack from the store. This is the primary coin sink in FUT. ## Prerequisites - `GET /packs/store` (already implemented in Phase 15) - `GET /club` already returns the `coins` balance ## Implementation Steps ### 1. Add `PackPurchaseRequest` to `src/models/packs.rs` ```rust pub struct PackPurchaseRequest { pub pack_id: String, } ``` ### 2. Validate pack exists in `src/services/packs.rs` ```rust pub fn get_pack_definition(pack_id: &str) -> Option<PackDefinition> ``` Load from `data/packs.json`; return `404 Not Found` if the pack_id is not recognised. ### 3. Validate player has enough coins - Load current club: `SELECT coins FROM clubs WHERE id = 1` - If `club.coins < pack.price` → return `400 Bad Request`: ```json { "error": "insufficient_coins", "required": 5000, "available": 2300 } ``` ### 4. Deduct coins atomically in a transaction ```sql BEGIN; UPDATE clubs SET coins = coins - ? WHERE id = 1 AND coins >= ?; -- if rows_affected == 0: rollback → return 400 INSERT INTO pack_history (pack_id, coins_spent, opened_at) VALUES (?, ?, CURRENT_TIMESTAMP); COMMIT; ``` Use `AND coins >= ?` as the atomic guard so there is no double-spend race. ### 5. Open the pack (reuse existing pack-open logic) Call the existing service function that picks random cards from the pack definition and adds them to the collection. ### 6. Return response ```json { "pack_id": "gold_standard", "coins_spent": 5000, "coins_remaining": 17400, "items": [ { "card_id": "card_123", "name": "Harry Kane", "overall": 88 } ] } ``` ### 7. Add `pack_history` migration Create `migrations/004_pack_history.sql`: ```sql CREATE TABLE IF NOT EXISTS pack_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, pack_id TEXT NOT NULL, coins_spent INTEGER NOT NULL, opened_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); ``` ### 8. Register route in `src/routes/packs.rs` ```rust .route("/packs/buy", post(buy_pack)) ``` ### 9. Write integration test - Seed club with 10 000 coins - `POST /packs/buy` with valid `pack_id` → 200, coins deducted, items returned - `POST /packs/buy` again when balance is too low → 400 `insufficient_coins` ## Acceptance Criteria - [ ] `POST /packs/buy` deducts coins from club balance - [ ] Returns 400 with `insufficient_coins` on low balance - [ ] Coin deduction is atomic (no partial state on failure) - [ ] Pack history recorded in `pack_history` table - [ ] Integration test covers happy path and insufficient-coins path - [ ] `cargo clippy -- -D warnings` passes
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: funman300/OpenFUT-Core#2