shopmanager-train-code / server /sqlite_store.py
hard007ik's picture
shop manage eng phase 2 first
048f186
"""
SQLite persistence: market gold purchase invoices (troy oz) and per-gram lots (FIFO for warehouse).
"""
from __future__ import annotations
import sqlite3
import time
from dataclasses import dataclass
from typing import List, Optional, Tuple
try:
from ..constants import GRAMS_PER_TROY_OZ, default_sqlite_path, get_sqlite_path
except ImportError:
# `python -c` / `import server` from the ShopManagerEng/ folder: `server` is a top module,
# so `..constants` is invalid. Parent package constants.py lives as a sibling of `server/`.
from constants import GRAMS_PER_TROY_OZ, default_sqlite_path, get_sqlite_path
def _db_path() -> str:
p = get_sqlite_path()
return p if p else default_sqlite_path()
def _connect() -> sqlite3.Connection:
path = _db_path()
from pathlib import Path
Path(path).parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(path, check_same_thread=False, timeout=30.0)
conn.row_factory = sqlite3.Row
return conn
def init_schema() -> None:
with _connect() as c:
c.executescript(
"""
CREATE TABLE IF NOT EXISTS gold_purchases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
episode_id TEXT NOT NULL,
product_name TEXT NOT NULL,
buy_price_usd REAL NOT NULL,
quantity_oz REAL NOT NULL,
cost_usd REAL NOT NULL,
ai_decision TEXT NOT NULL,
ai_confidence_pct REAL,
ai_reasoning TEXT,
target_price_usd REAL,
bought_at TEXT NOT NULL,
fund_before_usd REAL NOT NULL,
fund_after_usd REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_gold_purchases_episode
ON gold_purchases (episode_id);
CREATE TABLE IF NOT EXISTS gold_grams_lots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
purchase_id INTEGER NOT NULL,
episode_id TEXT NOT NULL,
product_name TEXT NOT NULL,
buy_price_usd_per_gram REAL NOT NULL,
quantity_grams_total REAL NOT NULL,
quantity_grams_remaining REAL NOT NULL,
bought_at TEXT NOT NULL,
FOREIGN KEY (purchase_id) REFERENCES gold_purchases (id)
);
CREATE INDEX IF NOT EXISTS idx_lots_episode_bought
ON gold_grams_lots (episode_id, bought_at, id);
"""
)
c.commit()
@dataclass
class PurchaseRow:
id: int
buy_price_usd: float
quantity_oz: float
cost_usd: float
target_price_usd: Optional[float]
fund_before_usd: float
fund_after_usd: float
bought_at: str
def record_gold_purchase(
episode_id: str,
product_name: str,
buy_price_usd: float,
quantity_oz: float,
cost_usd: float,
ai_decision: str,
ai_confidence_pct: Optional[float],
ai_reasoning: Optional[str],
target_price_usd: Optional[float],
fund_before_usd: float,
fund_after_usd: float,
) -> Tuple[int, int]:
"""
Inserts into gold_purchases and gold_grams_lots. Returns (purchase_id, lot_id).
"""
init_schema()
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
g_total = round(quantity_oz * GRAMS_PER_TROY_OZ, 6)
ppg = round(buy_price_usd / GRAMS_PER_TROY_OZ, 8) if GRAMS_PER_TROY_OZ > 0 else 0.0
ai_r = (ai_reasoning or "").strip() or None
with _connect() as c:
cur = c.execute(
"""
INSERT INTO gold_purchases (
episode_id, product_name, buy_price_usd, quantity_oz, cost_usd,
ai_decision, ai_confidence_pct, ai_reasoning, target_price_usd,
bought_at, fund_before_usd, fund_after_usd
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)
""",
(
episode_id,
product_name,
buy_price_usd,
quantity_oz,
cost_usd,
ai_decision,
ai_confidence_pct,
ai_r,
target_price_usd,
now,
fund_before_usd,
fund_after_usd,
),
)
purchase_id = int(cur.lastrowid)
cur2 = c.execute(
"""
INSERT INTO gold_grams_lots (
purchase_id, episode_id, product_name, buy_price_usd_per_gram,
quantity_grams_total, quantity_grams_remaining, bought_at
) VALUES (?,?,?,?,?,?,?)
""",
(
purchase_id,
episode_id,
product_name,
ppg,
g_total,
g_total,
now,
),
)
lot_id = int(cur2.lastrowid)
c.commit()
return purchase_id, lot_id
def fifo_consume_grams(
episode_id: str, grams_needed: float
) -> Tuple[bool, float, List[dict]]:
"""
Uses oldest lots first. Returns (ok, total_usd_cost, details).
"""
if grams_needed <= 0:
return True, 0.0, []
init_schema()
rem = float(grams_needed)
total_usd = 0.0
details: List[dict] = []
with _connect() as c:
cur = c.execute(
"""
SELECT id, quantity_grams_remaining, buy_price_usd_per_gram
FROM gold_grams_lots
WHERE episode_id = ? AND quantity_grams_remaining > 0.0000001
ORDER BY bought_at ASC, id ASC
""",
(episode_id,),
)
rows = cur.fetchall()
for row in rows:
if rem <= 1e-9:
break
lot_id = int(row["id"])
qrem = float(row["quantity_grams_remaining"])
ppg = float(row["buy_price_usd_per_gram"])
take = min(qrem, rem)
cost = take * ppg
new_rem = round(qrem - take, 6)
c.execute(
"UPDATE gold_grams_lots SET quantity_grams_remaining = ? WHERE id = ?",
(new_rem, lot_id),
)
rem -= take
total_usd += cost
details.append(
{
"lot_id": lot_id,
"grams": take,
"cost_usd": round(cost, 4),
}
)
c.commit()
if rem > 1e-5:
return False, 0.0, []
return True, round(total_usd, 4), details
def ensure_schema_once() -> None:
try:
init_schema()
except Exception:
pass