we are so back
This commit is contained in:
parent
11aa4cda16
commit
5c85411c69
94
app/data/test_data/dragon.csv
Normal file
94
app/data/test_data/dragon.csv
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
Name,Set code,Set name,Collector number,Foil,Rarity,Quantity,ManaBox ID,Scryfall ID,Purchase price,Misprint,Altered,Condition,Language,Purchase price currency
|
||||||
|
Undergrowth Leopard,TDM,Tarkir: Dragonstorm,165,foil,common,1,104307,67ab8f9a-b17c-452f-b4ef-a3f91909e3de,0.08,false,false,near_mint,en,USD
|
||||||
|
Gurmag Nightwatch,TDM,Tarkir: Dragonstorm,190,foil,common,1,104369,de731430-6bbf-4782-953e-b69c46353959,0.03,false,false,near_mint,en,USD
|
||||||
|
Mystic Monastery,TDM,Tarkir: Dragonstorm,262,foil,uncommon,1,104945,c7b8a01c-c400-47c7-8270-78902efe850e,0.21,false,false,near_mint,en,USD
|
||||||
|
Stormshriek Feral // Flush Out,TDM,Tarkir: Dragonstorm,124,foil,common,1,104447,0ec92c44-7cf0-48a5-a3ca-bc633496d887,0.1,false,false,near_mint,en,USD
|
||||||
|
Reigning Victor,TDM,Tarkir: Dragonstorm,216,foil,common,1,104334,a394112a-032b-4047-887a-6522cf7b83d5,0.02,false,false,near_mint,en,USD
|
||||||
|
Dragonbroods' Relic,TDM,Tarkir: Dragonstorm,140,foil,uncommon,1,104569,3d634087-77ba-4543-aa7a-8a3774d69cd7,0.13,false,false,near_mint,en,USD
|
||||||
|
Sagu Wildling // Roost Seek,TDM,Tarkir: Dragonstorm,306,foil,common,1,104903,b72ee8f9-5e79-4f77-ae7e-e4c274f78187,0.13,false,false,near_mint,en,USD
|
||||||
|
Sibsig Appraiser,TDM,Tarkir: Dragonstorm,56,foil,common,1,105135,670c5b96-bac6-449b-a2bd-cb43750d3911,0.05,false,false,near_mint,en,USD
|
||||||
|
Sage of the Fang,TDM,Tarkir: Dragonstorm,155,foil,uncommon,1,105123,1ebf4a9d-d90c-4017-9f00-fca89899f301,0.09,false,false,near_mint,en,USD
|
||||||
|
Snowmelt Stag,TDM,Tarkir: Dragonstorm,57,foil,common,1,104869,a6b3b131-704a-4586-84f8-db465cd4a277,0.04,false,false,near_mint,en,USD
|
||||||
|
Tranquil Cove,TDM,Tarkir: Dragonstorm,270,foil,common,1,104249,1c4efa6c-4f29-41cd-a728-bf0e479ace05,0.07,false,false,near_mint,en,USD
|
||||||
|
Rally the Monastery,TDM,Tarkir: Dragonstorm,19,foil,uncommon,1,104136,b56e0037-8143-4c13-83e1-0c3f44e685ea,0.22,false,false,near_mint,en,USD
|
||||||
|
Dragon's Prey,TDM,Tarkir: Dragonstorm,79,foil,common,1,104754,7a6004ff-4180-4332-8b51-960f8c7521d9,0.03,false,false,near_mint,en,USD
|
||||||
|
Ambling Stormshell,TDM,Tarkir: Dragonstorm,37,foil,rare,1,104942,c74d4a57-0f66-4965-9ed7-f88a08aa1d15,0.45,false,false,near_mint,en,USD
|
||||||
|
Mountain,TDM,Tarkir: Dragonstorm,275,foil,common,1,104397,fe0865ba-47c0-40bc-b0c6-e1ea5ae08a98,1.83,false,false,near_mint,en,USD
|
||||||
|
Mardu Devotee,TDM,Tarkir: Dragonstorm,16,foil,common,1,104366,da45e9b0-a4f6-413b-9e62-666c511eb5b0,0.09,false,false,near_mint,en,USD
|
||||||
|
Swiftwater Cliffs,TDM,Tarkir: Dragonstorm,268,foil,common,1,104361,ca53fb19-b8ca-485b-af1a-5117ae54bfe3,0.11,false,false,near_mint,en,USD
|
||||||
|
Adorned Crocodile,TDM,Tarkir: Dragonstorm,69,foil,common,1,105159,bb13a34b-6ac8-47cb-9e91-47106a585fc1,0.05,false,false,near_mint,en,USD
|
||||||
|
Dusyut Earthcarver,TDM,Tarkir: Dragonstorm,141,foil,common,1,104352,b98ecc96-f557-479a-8685-2b5487d5b407,0.02,false,false,near_mint,en,USD
|
||||||
|
Knockout Maneuver,TDM,Tarkir: Dragonstorm,147,foil,uncommon,1,105149,9d218831-2a41-46a3-8e9d-93462cae5cab,0.07,false,false,near_mint,en,USD
|
||||||
|
Roiling Dragonstorm,TDM,Tarkir: Dragonstorm,55,foil,uncommon,1,104280,455f4c96-684b-4b14-bd21-6799da2e1fa7,0.22,false,false,near_mint,en,USD
|
||||||
|
Dragonclaw Strike,TDM,Tarkir: Dragonstorm,180,foil,uncommon,1,105161,bc7692ef-7091-4365-85a8-1edbd374f279,0.12,false,false,near_mint,en,USD
|
||||||
|
Seize Opportunity,TDM,Tarkir: Dragonstorm,119,foil,common,1,104391,f7818d28-b9a5-4341-9adc-666070b8878d,0.03,false,false,near_mint,en,USD
|
||||||
|
Shock Brigade,TDM,Tarkir: Dragonstorm,120,foil,common,1,104700,66940466-8e9d-4a85-bfb0-e92189b7a121,0.11,false,false,near_mint,en,USD
|
||||||
|
Hardened Tactician,TDM,Tarkir: Dragonstorm,191,foil,uncommon,1,104780,86b225cb-5c45-4da1-a64e-b04091e483e8,0.43,false,false,near_mint,en,USD
|
||||||
|
Stormplain Detainment,TDM,Tarkir: Dragonstorm,28,foil,common,1,104135,39f3aab5-7b54-4b55-8114-c6f9f79c255d,0.04,false,false,near_mint,en,USD
|
||||||
|
Formation Breaker,TDM,Tarkir: Dragonstorm,143,foil,uncommon,1,105136,67ab8e8f-3ef6-4339-8c66-68c5aca4867a,0.08,false,false,near_mint,en,USD
|
||||||
|
Duty Beyond Death,TDM,Tarkir: Dragonstorm,10,foil,uncommon,1,104265,2e92640d-768b-4357-905f-bea017d351cc,1.11,false,false,near_mint,en,USD
|
||||||
|
Piercing Exhale,TDM,Tarkir: Dragonstorm,151,foil,common,1,104891,b2a0deb9-5bc3-42d5-9e1e-5f463d176aef,0.04,false,false,near_mint,en,USD
|
||||||
|
Trade Route Envoy,TDM,Tarkir: Dragonstorm,163,foil,common,1,105174,f0c89d95-d697-4cfa-9dfa-52d7adb96176,0.05,false,false,near_mint,en,USD
|
||||||
|
Thornwood Falls,TDM,Tarkir: Dragonstorm,269,foil,common,1,104376,ebb502c2-5fd0-46a9-b77d-010f4a942056,0.07,false,false,near_mint,en,USD
|
||||||
|
Kin-Tree Nurturer,TDM,Tarkir: Dragonstorm,83,foil,common,1,105124,2177ef64-28bf-4acf-b1f1-c1408f03c411,0.03,false,false,near_mint,en,USD
|
||||||
|
Rebellious Strike,TDM,Tarkir: Dragonstorm,20,foil,common,1,104949,c9bafe19-3bd6-4da0-b3e5-e0b89262504c,0.06,false,false,near_mint,en,USD
|
||||||
|
Scoured Barrens,TDM,Tarkir: Dragonstorm,267,foil,common,1,104346,b4b47b80-69ed-44b0-afa0-ca90206dc16d,0.06,false,false,near_mint,en,USD
|
||||||
|
Avenger of the Fallen,TDM,Tarkir: Dragonstorm,73,foil,rare,1,104984,d5397366-151f-46b0-b9b2-fa4d5bd892d8,0.68,false,false,near_mint,en,USD
|
||||||
|
Dismal Backwater,TDM,Tarkir: Dragonstorm,254,foil,common,1,104238,082b52c9-c46e-44d3-b723-546ba528e07b,0.07,false,false,near_mint,en,USD
|
||||||
|
Rediscover the Way,TDM,Tarkir: Dragonstorm,215,normal,rare,1,104313,79d6decf-afd5-4e96-b87e-fd7ab7e3c068,0.19,false,false,near_mint,en,USD
|
||||||
|
Auroral Procession,TDM,Tarkir: Dragonstorm,169,normal,uncommon,1,104701,672f94ad-65d6-4c7d-925d-165ef264626f,0.22,false,false,near_mint,en,USD
|
||||||
|
Runescale Stormbrood // Chilling Screech,TDM,Tarkir: Dragonstorm,316,normal,uncommon,1,104733,72e8f916-5a01-4918-bcb5-7fd69fe32785,0.31,false,false,near_mint,en,USD
|
||||||
|
Stadium Headliner,TDM,Tarkir: Dragonstorm,122,normal,rare,1,104552,37d4ab2a-a06a-4768-b5e1-e1def957d7f4,0.44,false,false,near_mint,en,USD
|
||||||
|
Nature's Rhythm,TDM,Tarkir: Dragonstorm,150,normal,rare,1,104460,1397d904-c51d-451e-8505-7f3118acc1f6,3.08,false,false,near_mint,en,USD
|
||||||
|
Karakyk Guardian,TDM,Tarkir: Dragonstorm,198,normal,uncommon,1,104859,a4c77b08-c3f6-4458-8636-f226f9843b6d,0.08,false,false,near_mint,en,USD
|
||||||
|
"Anafenza, Unyielding Lineage",TDM,Tarkir: Dragonstorm,2,normal,rare,1,104258,29957f49-9a6b-42f6-b2fb-b48f653ab725,0.22,false,false,near_mint,en,USD
|
||||||
|
"Narset, Jeskai Waymaster",TDM,Tarkir: Dragonstorm,209,normal,rare,1,103995,6b77cbc1-dbc8-44d9-aa29-15cbb19afecd,0.22,false,false,near_mint,en,USD
|
||||||
|
Stillness in Motion,TDM,Tarkir: Dragonstorm,59,normal,rare,1,104864,a6289251-17e4-4987-96b9-2fb1a8f90e2a,0.17,false,false,near_mint,en,USD
|
||||||
|
Thunder of Unity,TDM,Tarkir: Dragonstorm,231,normal,rare,1,104671,5c953b36-f5e4-4258-91cb-f07e799321f7,0.14,false,false,near_mint,en,USD
|
||||||
|
The Sibsig Ceremony,TDM,Tarkir: Dragonstorm,340,normal,rare,1,104719,6daa156c-478f-47dd-9284-b95e82ccfd68,0.67,false,false,near_mint,en,USD
|
||||||
|
Tersa Lightshatter,TDM,Tarkir: Dragonstorm,127,normal,rare,1,104825,99e96b34-b1c4-4647-a38e-2cf1aedaaace,2.31,false,false,near_mint,en,USD
|
||||||
|
Forest,TDM,Tarkir: Dragonstorm,286,normal,common,1,104324,8e3e83d2-96ba-4d5c-a1ed-6c08a90b339c,0.07,false,false,near_mint,en,USD
|
||||||
|
Windcrag Siege,TDM,Tarkir: Dragonstorm,235,normal,rare,1,104534,31a8329b-23a1-4c49-a579-a5da8d01435a,1.68,false,false,near_mint,en,USD
|
||||||
|
Mountain,TDM,Tarkir: Dragonstorm,284,normal,common,1,104274,3df7c206-97b6-49d7-ba01-7a35fd8c61d9,0.05,false,false,near_mint,en,USD
|
||||||
|
Inevitable Defeat,TDM,Tarkir: Dragonstorm,194,normal,rare,1,103997,9d677980-b608-407e-9f17-790a81263f15,0.28,false,false,near_mint,en,USD
|
||||||
|
Dalkovan Encampment,TDM,Tarkir: Dragonstorm,394,normal,rare,1,104670,5af006f6-135e-4ea0-8ce4-7824934e87da,0.72,false,false,near_mint,en,USD
|
||||||
|
Host of the Hereafter,TDM,Tarkir: Dragonstorm,193,normal,uncommon,1,104448,0f182957-8133-45a7-80a3-1944bead4d43,0.14,false,false,near_mint,en,USD
|
||||||
|
"Sarkhan, Dragon Ascendant",TDM,Tarkir: Dragonstorm,118,normal,rare,1,104003,c2200646-7b7c-489d-bbae-16b03e1d7fb2,0.32,false,false,near_mint,en,USD
|
||||||
|
Stormscale Scion,TDM,Tarkir: Dragonstorm,123,normal,mythic,1,103987,0ac43386-bd32-425c-8776-cec00b064cbc,6.78,false,false,near_mint,en,USD
|
||||||
|
Dragon Sniper,TDM,Tarkir: Dragonstorm,139,normal,uncommon,1,105120,074b1e00-45bb-4436-8f5e-058512b2d08a,0.25,false,false,near_mint,en,USD
|
||||||
|
Island,TDM,Tarkir: Dragonstorm,273,normal,common,1,104276,4208e66c-8c98-4c48-ab07-8523c0b26ca4,1.02,false,false,near_mint,en,USD
|
||||||
|
Yathan Roadwatcher,TDM,Tarkir: Dragonstorm,236,normal,rare,1,104800,8e77339b-dd82-481c-9ee2-4156ca69ad35,0.14,false,false,near_mint,en,USD
|
||||||
|
Nomad Outpost,TDM,Tarkir: Dragonstorm,263,normal,uncommon,1,104868,a68fbeaa-941f-4d53-becd-f93ed22b9a54,0.12,false,false,near_mint,en,USD
|
||||||
|
Sage of the Skies,TDM,Tarkir: Dragonstorm,22,normal,rare,1,104710,6ade6918-6d1d-448d-ab56-93996051e9a9,0.21,false,false,near_mint,en,USD
|
||||||
|
Kheru Goldkeeper,TDM,Tarkir: Dragonstorm,199,normal,uncommon,1,104798,8d11183a-57f5-4ddb-8a6e-15fff704b114,0.18,false,false,near_mint,en,USD
|
||||||
|
All-Out Assault,TDM,Tarkir: Dragonstorm,167,normal,mythic,1,104348,b74876d8-f6a6-4b47-b960-b01a331bab01,4.11,false,false,near_mint,en,USD
|
||||||
|
Winternight Stories,TDM,Tarkir: Dragonstorm,67,normal,rare,1,104693,64d9367c-f50c-4568-aa63-6760c44ecaeb,0.44,false,false,near_mint,en,USD
|
||||||
|
New Way Forward,TDM,Tarkir: Dragonstorm,211,normal,rare,1,104996,d9d48f9e-79f0-478c-9db0-ff7ac4a8f401,0.17,false,false,near_mint,en,USD
|
||||||
|
Strategic Betrayal,TDM,Tarkir: Dragonstorm,94,normal,uncommon,1,105145,95617742-548d-464a-bb89-a858ffa9018f,0.18,false,false,near_mint,en,USD
|
||||||
|
Opulent Palace,TDM,Tarkir: Dragonstorm,264,normal,uncommon,1,104491,21cb3b3b-0738-4c2e-a3fc-927fd6b9d3fb,0.14,false,false,near_mint,en,USD
|
||||||
|
United Battlefront,TDM,Tarkir: Dragonstorm,32,normal,rare,1,104370,dff398be-4ba4-4976-9acc-be99d2e07a61,0.51,false,false,near_mint,en,USD
|
||||||
|
Temur Battlecrier,TDM,Tarkir: Dragonstorm,228,normal,rare,1,104309,72184791-0767-4108-920c-763e92dae2d4,0.68,false,false,near_mint,en,USD
|
||||||
|
"Kotis, the Fangkeeper",TDM,Tarkir: Dragonstorm,362,normal,rare,1,104388,f70098f2-e5a8-4056-b5b3-1229fc290c51,0.48,false,false,near_mint,en,USD
|
||||||
|
Forest,TDM,Tarkir: Dragonstorm,285,normal,common,1,104317,8100bceb-ffba-487a-bb45-4fe2a156a8dc,0.06,false,false,near_mint,en,USD
|
||||||
|
Dragonfire Blade,TDM,Tarkir: Dragonstorm,240,normal,rare,1,104427,031afea3-fbfb-4663-a8cc-9b7eb7b16020,0.64,false,false,near_mint,en,USD
|
||||||
|
Great Arashin City,TDM,Tarkir: Dragonstorm,257,normal,rare,1,105033,ecba23b6-9f3a-431e-bc22-f1fb04d27b68,0.33,false,false,near_mint,en,USD
|
||||||
|
Smile at Death,TDM,Tarkir: Dragonstorm,24,normal,mythic,1,104000,ae2da18f-0d7d-446c-b463-8bf170ed95da,3.51,false,false,near_mint,en,USD
|
||||||
|
Maelstrom of the Spirit Dragon,TDM,Tarkir: Dragonstorm,260,normal,rare,1,104359,c4e90bfb-d9a5-48a9-9ff9-b0f50a813eee,1.31,false,false,near_mint,en,USD
|
||||||
|
Eshki Dragonclaw,TDM,Tarkir: Dragonstorm,182,normal,rare,1,104445,0d369c44-78ee-4f3c-bf2b-cddba7fe26d4,0.19,false,false,near_mint,en,USD
|
||||||
|
Skirmish Rhino,TDM,Tarkir: Dragonstorm,224,normal,uncommon,1,103992,4a2e9ba1-c254-41e3-9845-4e81f9fec38d,0.15,false,false,near_mint,en,USD
|
||||||
|
"Teval, Arbiter of Virtue",TDM,Tarkir: Dragonstorm,373,normal,mythic,1,104332,a19c38bc-946c-438a-ac8b-f59ff0b4c613,7.06,false,false,near_mint,en,USD
|
||||||
|
"Ureni, the Song Unending",TDM,Tarkir: Dragonstorm,233,normal,mythic,1,104253,227802c0-4ff6-43a8-a850-ed0f546dc5ac,3.79,false,false,near_mint,en,USD
|
||||||
|
Hardened Tactician,TDM,Tarkir: Dragonstorm,191,normal,uncommon,1,104780,86b225cb-5c45-4da1-a64e-b04091e483e8,1.0,false,false,near_mint,en,USD
|
||||||
|
Sandsteppe Citadel,TDM,Tarkir: Dragonstorm,266,normal,uncommon,1,104603,47f47e7f-39ba-4807-8e32-7262a61dfbba,0.13,false,false,near_mint,en,USD
|
||||||
|
"Kotis, the Fangkeeper",TDM,Tarkir: Dragonstorm,202,normal,rare,1,104364,d3736f17-f80b-4b2c-b919-2c963bc14682,0.28,false,false,near_mint,en,USD
|
||||||
|
Magmatic Hellkite,TDM,Tarkir: Dragonstorm,111,normal,rare,1,104895,b3b3aec8-d931-4c7f-86b5-1e7dfb717b59,0.56,false,false,near_mint,en,USD
|
||||||
|
Dalkovan Encampment,TDM,Tarkir: Dragonstorm,253,normal,rare,1,104822,98ad5f0c-8775-4e89-8e92-84a6ade93e35,0.38,false,false,near_mint,en,USD
|
||||||
|
Ambling Stormshell,TDM,Tarkir: Dragonstorm,37,normal,rare,1,104942,c74d4a57-0f66-4965-9ed7-f88a08aa1d15,0.18,false,false,near_mint,en,USD
|
||||||
|
Hollowmurk Siege,TDM,Tarkir: Dragonstorm,192,normal,rare,1,104668,5ac0e136-8877-4bfc-a831-2bf7b7b5ad1e,0.53,false,false,near_mint,en,USD
|
||||||
|
Avenger of the Fallen,TDM,Tarkir: Dragonstorm,73,normal,rare,1,104984,d5397366-151f-46b0-b9b2-fa4d5bd892d8,0.34,false,false,near_mint,en,USD
|
||||||
|
Mystic Monastery,TDM,Tarkir: Dragonstorm,262,normal,uncommon,1,104945,c7b8a01c-c400-47c7-8270-78902efe850e,0.11,false,false,near_mint,en,USD
|
||||||
|
Songcrafter Mage,TDM,Tarkir: Dragonstorm,225,normal,rare,1,104813,9523bc07-49e5-409c-ae6b-b28e305eef36,0.35,false,false,near_mint,en,USD
|
||||||
|
Misty Rainforest,SPG,Special Guests,111,normal,mythic,1,104321,894105c4-d3ce-4d38-855b-24aa47b112c1,32.31,false,false,near_mint,en,USD
|
||||||
|
Tempest Hawk,TDM,Tarkir: Dragonstorm,31,normal,common,3,104587,422f9453-ab12-4e3c-8c51-be87391395a1,0.92,false,false,near_mint,en,USD
|
||||||
|
Duty Beyond Death,TDM,Tarkir: Dragonstorm,10,normal,uncommon,2,104265,2e92640d-768b-4357-905f-bea017d351cc,0.33,false,false,near_mint,en,USD
|
||||||
|
Heritage Reclamation,TDM,Tarkir: Dragonstorm,145,normal,common,2,104636,4f8fee37-a050-4329-8b10-46d150e7a95e,0.2,false,false,near_mint,en,USD
|
|
44
app/main.py
44
app/main.py
@ -66,8 +66,8 @@ async def lifespan(app: FastAPI):
|
|||||||
logger.info("Most recent prices updated successfully")
|
logger.info("Most recent prices updated successfully")
|
||||||
|
|
||||||
# Create default customer, vendor, and marketplace
|
# Create default customer, vendor, and marketplace
|
||||||
inv_data_init = await data_init_service.initialize_inventory_data(db)
|
#inv_data_init = await data_init_service.initialize_inventory_data(db)
|
||||||
logger.info(f"Inventory data initialization results: {inv_data_init}")
|
#logger.info(f"Inventory data initialization results: {inv_data_init}")
|
||||||
# Start the scheduler
|
# Start the scheduler
|
||||||
scheduler = service_manager.get_service('scheduler')
|
scheduler = service_manager.get_service('scheduler')
|
||||||
await scheduler.refresh_tcgplayer_inventory_table(db)
|
await scheduler.refresh_tcgplayer_inventory_table(db)
|
||||||
@ -115,6 +115,46 @@ async def read_app_js():
|
|||||||
raise HTTPException(status_code=404, detail="App.js file not found")
|
raise HTTPException(status_code=404, detail="App.js file not found")
|
||||||
return FileResponse(js_path)
|
return FileResponse(js_path)
|
||||||
|
|
||||||
|
# Serve manabox.html
|
||||||
|
@app.get("/manabox.html")
|
||||||
|
async def read_manabox_html():
|
||||||
|
html_path = Path('app/static/manabox.html')
|
||||||
|
if not html_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Manabox.html file not found")
|
||||||
|
return FileResponse(html_path)
|
||||||
|
|
||||||
|
# Serve manabox.js
|
||||||
|
@app.get("/manabox.js")
|
||||||
|
async def read_manabox_js():
|
||||||
|
js_path = Path('app/static/manabox.js')
|
||||||
|
if not js_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Manabox.js file not found")
|
||||||
|
return FileResponse(js_path)
|
||||||
|
|
||||||
|
# serve transactions.html
|
||||||
|
@app.get("/transactions.html")
|
||||||
|
async def read_transactions_html():
|
||||||
|
html_path = Path('app/static/transactions.html')
|
||||||
|
if not html_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Transaction.html file not found")
|
||||||
|
return FileResponse(html_path)
|
||||||
|
|
||||||
|
# serve transactions.js
|
||||||
|
@app.get("/transactions.js")
|
||||||
|
async def read_transactions_js():
|
||||||
|
js_path = Path('app/static/transactions.js')
|
||||||
|
if not js_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Transaction.js file not found")
|
||||||
|
return FileResponse(js_path)
|
||||||
|
|
||||||
|
# serve styles.css
|
||||||
|
@app.get("/styles.css")
|
||||||
|
async def read_styles_css():
|
||||||
|
css_path = Path('app/static/styles.css')
|
||||||
|
if not css_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Styles.css file not found")
|
||||||
|
return FileResponse(css_path)
|
||||||
|
|
||||||
# Configure CORS with specific origins in production
|
# Configure CORS with specific origins in production
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
|
@ -252,6 +252,9 @@ class Vendor(Base):
|
|||||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
transactions = relationship("Transaction", back_populates="vendors")
|
||||||
|
|
||||||
class Customer(Base):
|
class Customer(Base):
|
||||||
__tablename__ = "customers"
|
__tablename__ = "customers"
|
||||||
|
|
||||||
@ -260,6 +263,10 @@ class Customer(Base):
|
|||||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||||
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
transactions = relationship("Transaction", back_populates="customers")
|
||||||
|
|
||||||
class Transaction(Base):
|
class Transaction(Base):
|
||||||
__tablename__ = "transactions"
|
__tablename__ = "transactions"
|
||||||
|
|
||||||
@ -277,6 +284,9 @@ class Transaction(Base):
|
|||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
transaction_items = relationship("TransactionItem", back_populates="transaction")
|
transaction_items = relationship("TransactionItem", back_populates="transaction")
|
||||||
|
vendors = relationship("Vendor", back_populates="transactions")
|
||||||
|
customers = relationship("Customer", back_populates="transactions")
|
||||||
|
marketplaces = relationship("Marketplace", back_populates="transactions")
|
||||||
|
|
||||||
class Marketplace(Base):
|
class Marketplace(Base):
|
||||||
__tablename__ = "marketplaces"
|
__tablename__ = "marketplaces"
|
||||||
@ -289,7 +299,7 @@ class Marketplace(Base):
|
|||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
listings = relationship("MarketplaceListing", back_populates="marketplace")
|
listings = relationship("MarketplaceListing", back_populates="marketplace")
|
||||||
|
transactions = relationship("Transaction", back_populates="marketplaces")
|
||||||
class MarketplaceListing(Base):
|
class MarketplaceListing(Base):
|
||||||
__tablename__ = "marketplace_listings"
|
__tablename__ = "marketplace_listings"
|
||||||
|
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from datetime import datetime
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import and_, func
|
||||||
from app.db.database import get_db
|
from app.db.database import get_db
|
||||||
from app.services.service_manager import ServiceManager
|
from app.services.service_manager import ServiceManager
|
||||||
from app.contexts.inventory_item import InventoryItemContextFactory
|
from app.contexts.inventory_item import InventoryItemContextFactory
|
||||||
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, TransactionResponse
|
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, SealedExpectedValueCreate, GetAllTransactionsResponse, TransactionResponse, TransactionItemResponse, InventoryItemResponse, TCGPlayerProductResponse, OpenEventResponse, OpenEventCreate, OpenEventResultingItemsResponse, OpenEventsForInventoryItemResponse
|
||||||
|
from app.models.inventory_management import Transaction
|
||||||
|
from app.models.tcgplayer_products import TCGPlayerProduct
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
router = APIRouter(prefix="/inventory")
|
router = APIRouter(prefix="/inventory")
|
||||||
|
|
||||||
service_manager = ServiceManager()
|
service_manager = ServiceManager()
|
||||||
@ -155,6 +161,14 @@ async def create_vendor(
|
|||||||
vendor = await inventory_service.create_vendor(db, vendor_name)
|
vendor = await inventory_service.create_vendor(db, vendor_name)
|
||||||
return vendor
|
return vendor
|
||||||
|
|
||||||
|
@router.get("/vendors")
|
||||||
|
async def get_vendors(
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
vendors = await inventory_service.get_vendors(db)
|
||||||
|
return vendors
|
||||||
|
|
||||||
@router.post("/marketplaces")
|
@router.post("/marketplaces")
|
||||||
async def create_marketplace(
|
async def create_marketplace(
|
||||||
marketplace_name: str,
|
marketplace_name: str,
|
||||||
@ -162,4 +176,313 @@ async def create_marketplace(
|
|||||||
):
|
):
|
||||||
inventory_service = service_manager.get_service("inventory")
|
inventory_service = service_manager.get_service("inventory")
|
||||||
marketplace = await inventory_service.create_marketplace(db, marketplace_name)
|
marketplace = await inventory_service.create_marketplace(db, marketplace_name)
|
||||||
return marketplace
|
return marketplace
|
||||||
|
|
||||||
|
@router.get("/marketplaces")
|
||||||
|
async def get_marketplaces(
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
marketplaces = await inventory_service.get_marketplaces(db)
|
||||||
|
return marketplaces
|
||||||
|
|
||||||
|
@router.get("/products/search")
|
||||||
|
async def get_products(q: str, db: Session = Depends(get_db)):
|
||||||
|
query = ' & '.join(q.lower().split()) # This ensures all terms must match
|
||||||
|
|
||||||
|
products = db.query(TCGPlayerProduct).filter(
|
||||||
|
func.to_tsvector('english', TCGPlayerProduct.name)
|
||||||
|
.op('@@')(func.to_tsquery('english', query))
|
||||||
|
).all()
|
||||||
|
|
||||||
|
return products
|
||||||
|
|
||||||
|
@router.get("/products/{product_id}/expected-value")
|
||||||
|
async def get_expected_value(
|
||||||
|
product_id: int,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
expected_value = await inventory_service.get_expected_value(db, product_id)
|
||||||
|
return expected_value
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/products/expected-value")
|
||||||
|
async def create_expected_value(
|
||||||
|
expected_value_data: SealedExpectedValueCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
expected_value = await inventory_service.create_expected_value(db, expected_value_data)
|
||||||
|
return expected_value
|
||||||
|
|
||||||
|
@router.post("/transactions/purchase")
|
||||||
|
async def create_purchase_transaction(
|
||||||
|
transaction_data: PurchaseTransactionCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
transaction = await inventory_service.create_purchase_transaction(db, transaction_data)
|
||||||
|
return transaction
|
||||||
|
|
||||||
|
@router.get("/transactions")
|
||||||
|
async def get_transactions(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
total = db.query(func.count(Transaction.id)).scalar()
|
||||||
|
transactions = await inventory_service.get_transactions(db, skip, limit)
|
||||||
|
return GetAllTransactionsResponse(
|
||||||
|
total=total,
|
||||||
|
transactions=[TransactionResponse(
|
||||||
|
id=transaction.id,
|
||||||
|
vendor_id=transaction.vendor_id,
|
||||||
|
customer_id=transaction.customer_id,
|
||||||
|
marketplace_id=transaction.marketplace_id,
|
||||||
|
transaction_type=transaction.transaction_type,
|
||||||
|
transaction_date=transaction.transaction_date,
|
||||||
|
transaction_total_amount=transaction.transaction_total_amount,
|
||||||
|
transaction_notes=transaction.transaction_notes,
|
||||||
|
created_at=transaction.created_at,
|
||||||
|
updated_at=transaction.updated_at,
|
||||||
|
transaction_items=[TransactionItemResponse(
|
||||||
|
id=transaction_item.id,
|
||||||
|
transaction_id=transaction_item.transaction_id,
|
||||||
|
inventory_item_id=transaction_item.inventory_item_id,
|
||||||
|
unit_price=transaction_item.unit_price,
|
||||||
|
created_at=transaction_item.created_at,
|
||||||
|
updated_at=transaction_item.updated_at,
|
||||||
|
deleted_at=transaction_item.deleted_at
|
||||||
|
) for transaction_item in transaction.transaction_items]
|
||||||
|
) for transaction in transactions]
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/transactions/{transaction_id}")
|
||||||
|
async def get_transaction(
|
||||||
|
transaction_id: int,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
transaction = await inventory_service.get_transaction(db, transaction_id)
|
||||||
|
if not transaction:
|
||||||
|
raise HTTPException(status_code=404, detail="Transaction not found")
|
||||||
|
return TransactionResponse(
|
||||||
|
id=transaction.id,
|
||||||
|
vendor_id=transaction.vendor_id,
|
||||||
|
customer_id=transaction.customer_id,
|
||||||
|
marketplace_id=transaction.marketplace_id,
|
||||||
|
transaction_type=transaction.transaction_type,
|
||||||
|
transaction_date=transaction.transaction_date,
|
||||||
|
transaction_total_amount=transaction.transaction_total_amount,
|
||||||
|
transaction_notes=transaction.transaction_notes,
|
||||||
|
created_at=transaction.created_at,
|
||||||
|
updated_at=transaction.updated_at,
|
||||||
|
transaction_items=[TransactionItemResponse(
|
||||||
|
id=transaction_item.id,
|
||||||
|
transaction_id=transaction_item.transaction_id,
|
||||||
|
inventory_item_id=transaction_item.inventory_item_id,
|
||||||
|
unit_price=transaction_item.unit_price,
|
||||||
|
created_at=transaction_item.created_at,
|
||||||
|
updated_at=transaction_item.updated_at,
|
||||||
|
deleted_at=transaction_item.deleted_at
|
||||||
|
) for transaction_item in transaction.transaction_items]
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/items/{inventory_item_id}")
|
||||||
|
async def get_inventory_item(
|
||||||
|
inventory_item_id: int,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
|
||||||
|
marketplace_listing_service = service_manager.get_service("marketplace_listing")
|
||||||
|
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
|
||||||
|
marketplace_listing = await marketplace_listing_service.get_marketplace_listing(db, inventory_item, marketplace)
|
||||||
|
if marketplace_listing is None:
|
||||||
|
listed_price = None
|
||||||
|
recommended_price = None
|
||||||
|
marketplace_listing_id = None
|
||||||
|
else:
|
||||||
|
if marketplace_listing.listed_price is not None:
|
||||||
|
listed_price = marketplace_listing.listed_price.price if marketplace_listing.listed_price.price is not None else None
|
||||||
|
else:
|
||||||
|
listed_price = None
|
||||||
|
if marketplace_listing.recommended_price is not None:
|
||||||
|
recommended_price = marketplace_listing.recommended_price.price if marketplace_listing.recommended_price.price is not None else None
|
||||||
|
else:
|
||||||
|
recommended_price = None
|
||||||
|
marketplace_listing_id = marketplace_listing.id
|
||||||
|
return InventoryItemResponse(
|
||||||
|
id=inventory_item.id,
|
||||||
|
physical_item_id=inventory_item.physical_item_id,
|
||||||
|
cost_basis=inventory_item.cost_basis,
|
||||||
|
parent_id=inventory_item.parent_id,
|
||||||
|
created_at=inventory_item.created_at,
|
||||||
|
updated_at=inventory_item.updated_at,
|
||||||
|
item_type=inventory_item.physical_item.item_type,
|
||||||
|
listed_price=listed_price,
|
||||||
|
recommended_price=recommended_price,
|
||||||
|
marketplace_listing_id=marketplace_listing_id,
|
||||||
|
product=TCGPlayerProductResponse(
|
||||||
|
id=inventory_item.physical_item.product_direct.id,
|
||||||
|
tcgplayer_product_id=inventory_item.physical_item.product_direct.tcgplayer_product_id,
|
||||||
|
name=inventory_item.physical_item.product_direct.name,
|
||||||
|
image_url=inventory_item.physical_item.product_direct.image_url,
|
||||||
|
category_id=inventory_item.physical_item.product_direct.category_id,
|
||||||
|
group_id=inventory_item.physical_item.product_direct.group_id,
|
||||||
|
url=inventory_item.physical_item.product_direct.url,
|
||||||
|
market_price=inventory_item.physical_item.product_direct.most_recent_tcgplayer_price.market_price,
|
||||||
|
category_name=inventory_item.physical_item.product_direct.category.name,
|
||||||
|
group_name=inventory_item.physical_item.product_direct.group.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.post("/items/{inventory_item_id}/open")
|
||||||
|
async def open_box_or_case(
|
||||||
|
open_event_data: OpenEventCreate,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
inventory_item = await inventory_service.get_inventory_item(db, open_event_data.inventory_item_id)
|
||||||
|
file_service = service_manager.get_service("file")
|
||||||
|
files = [await file_service.get_file(db, file_id) for file_id in open_event_data.manabox_file_upload_ids]
|
||||||
|
if inventory_item.physical_item.item_type == "box":
|
||||||
|
box_service = service_manager.get_service("box")
|
||||||
|
open_event = await box_service.open_box(db, inventory_item.physical_item, files)
|
||||||
|
return OpenEventResponse(
|
||||||
|
id=open_event.id,
|
||||||
|
source_item_id=open_event.source_item_id,
|
||||||
|
created_at=open_event.created_at,
|
||||||
|
updated_at=open_event.updated_at
|
||||||
|
)
|
||||||
|
elif inventory_item.physical_item.item_type == "case":
|
||||||
|
case_service = service_manager.get_service("case")
|
||||||
|
open_event = await case_service.open_case(db, inventory_item.physical_item, files)
|
||||||
|
return OpenEventResponse(
|
||||||
|
id=open_event.id,
|
||||||
|
source_item_id=open_event.source_item_id,
|
||||||
|
created_at=open_event.created_at,
|
||||||
|
updated_at=open_event.updated_at
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid item type")
|
||||||
|
|
||||||
|
@router.get("/items/{inventory_item_id}/open-events")
|
||||||
|
async def get_open_events(
|
||||||
|
inventory_item_id: int,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
|
||||||
|
|
||||||
|
# Don't return open events for cards
|
||||||
|
if inventory_item.physical_item.item_type == 'card':
|
||||||
|
return OpenEventsForInventoryItemResponse(open_events=[])
|
||||||
|
|
||||||
|
open_events = await inventory_service.get_open_events_for_inventory_item(db, inventory_item)
|
||||||
|
return OpenEventsForInventoryItemResponse(
|
||||||
|
open_events=[OpenEventResponse(
|
||||||
|
id=open_event.id,
|
||||||
|
source_item_id=open_event.source_item_id,
|
||||||
|
created_at=open_event.created_at,
|
||||||
|
updated_at=open_event.updated_at
|
||||||
|
) for open_event in open_events]
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/items/{inventory_item_id}/open-events/{open_event_id}/resulting-items", response_model=List[InventoryItemResponse])
|
||||||
|
async def get_resulting_items(
|
||||||
|
inventory_item_id: int,
|
||||||
|
open_event_id: int,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
|
||||||
|
open_event = await inventory_service.get_open_event(db, inventory_item, open_event_id)
|
||||||
|
resulting_items = await inventory_service.get_resulting_items_for_open_event(db, open_event)
|
||||||
|
marketplace_listing_service = service_manager.get_service("marketplace_listing")
|
||||||
|
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
|
||||||
|
marketplace_listing = await marketplace_listing_service.get_marketplace_listing(db, inventory_item, marketplace)
|
||||||
|
if marketplace_listing is None:
|
||||||
|
listed_price = None
|
||||||
|
recommended_price = None
|
||||||
|
marketplace_listing_id = None
|
||||||
|
else:
|
||||||
|
if marketplace_listing.listed_price is not None:
|
||||||
|
listed_price = marketplace_listing.listed_price.price if marketplace_listing.listed_price.price is not None else None
|
||||||
|
else:
|
||||||
|
listed_price = None
|
||||||
|
if marketplace_listing.recommended_price is not None:
|
||||||
|
recommended_price = marketplace_listing.recommended_price.price if marketplace_listing.recommended_price.price is not None else None
|
||||||
|
else:
|
||||||
|
recommended_price = None
|
||||||
|
marketplace_listing_id = marketplace_listing.id
|
||||||
|
return [InventoryItemResponse(
|
||||||
|
id=resulting_item.id,
|
||||||
|
physical_item_id=resulting_item.physical_item_id,
|
||||||
|
cost_basis=resulting_item.cost_basis,
|
||||||
|
parent_id=resulting_item.parent_id,
|
||||||
|
product=TCGPlayerProductResponse(
|
||||||
|
id=resulting_item.physical_item.product_direct.id,
|
||||||
|
tcgplayer_product_id=resulting_item.physical_item.product_direct.tcgplayer_product_id,
|
||||||
|
name=resulting_item.physical_item.product_direct.name,
|
||||||
|
image_url=resulting_item.physical_item.product_direct.image_url,
|
||||||
|
category_id=resulting_item.physical_item.product_direct.category_id,
|
||||||
|
group_id=resulting_item.physical_item.product_direct.group_id,
|
||||||
|
url=resulting_item.physical_item.product_direct.url,
|
||||||
|
market_price=resulting_item.physical_item.product_direct.most_recent_tcgplayer_price.market_price,
|
||||||
|
category_name=resulting_item.physical_item.product_direct.category.name,
|
||||||
|
group_name=resulting_item.physical_item.product_direct.group.name
|
||||||
|
),
|
||||||
|
item_type=resulting_item.physical_item.item_type,
|
||||||
|
marketplace_listing_id=marketplace_listing_id,
|
||||||
|
listed_price=listed_price,
|
||||||
|
recommended_price=recommended_price,
|
||||||
|
created_at=resulting_item.created_at,
|
||||||
|
updated_at=resulting_item.updated_at) for resulting_item in resulting_items]
|
||||||
|
|
||||||
|
@router.post("/items/{inventory_item_id}/open-events/{open_event_id}/create-listings")
|
||||||
|
async def create_marketplace_listings(
|
||||||
|
inventory_item_id: int,
|
||||||
|
open_event_id: int,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
|
||||||
|
open_event = await inventory_service.get_open_event(db, inventory_item, open_event_id)
|
||||||
|
resulting_items = await inventory_service.get_resulting_items_for_open_event(db, open_event)
|
||||||
|
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
|
||||||
|
marketplace_listing_service = service_manager.get_service("marketplace_listing")
|
||||||
|
for resulting_item in resulting_items:
|
||||||
|
await marketplace_listing_service.create_marketplace_listing(db, resulting_item, marketplace)
|
||||||
|
return {"message": f"{len(resulting_items)} marketplace listings created successfully"}
|
||||||
|
|
||||||
|
@router.post("/items/{inventory_item_id}/open-events/{open_event_id}/confirm-listings")
|
||||||
|
async def confirm_listings(
|
||||||
|
inventory_item_id: int,
|
||||||
|
open_event_id: int,
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
inventory_service = service_manager.get_service("inventory")
|
||||||
|
inventory_item = await inventory_service.get_inventory_item(db, inventory_item_id)
|
||||||
|
open_event = await inventory_service.get_open_event(db, inventory_item, open_event_id)
|
||||||
|
marketplace_listing_service = service_manager.get_service("marketplace_listing")
|
||||||
|
marketplace = await inventory_service.create_marketplace(db, "Tcgplayer")
|
||||||
|
try:
|
||||||
|
csv_string = await marketplace_listing_service.confirm_listings(db, open_event, marketplace)
|
||||||
|
if not csv_string:
|
||||||
|
raise ValueError("No CSV data generated")
|
||||||
|
|
||||||
|
# Create a streaming response with the CSV data
|
||||||
|
return StreamingResponse(
|
||||||
|
iter([csv_string]),
|
||||||
|
media_type="text/csv",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f"attachment; filename=tcgplayer_add_file_{open_event.id}_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Form
|
from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, Form, BackgroundTasks
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.db.database import get_db
|
from app.db.database import get_db
|
||||||
from app.services.service_manager import ServiceManager
|
from app.services.service_manager import ServiceManager
|
||||||
@ -50,6 +50,7 @@ def is_valid_csv(file: UploadFile) -> tuple[bool, str]:
|
|||||||
|
|
||||||
@router.post("/process-csv")
|
@router.post("/process-csv")
|
||||||
async def process_manabox_csv(
|
async def process_manabox_csv(
|
||||||
|
background_tasks: BackgroundTasks,
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
source: str = Form(...),
|
source: str = Form(...),
|
||||||
description: str = Form(...),
|
description: str = Form(...),
|
||||||
@ -72,7 +73,7 @@ async def process_manabox_csv(
|
|||||||
|
|
||||||
manabox_service = service_manager.get_service("manabox")
|
manabox_service = service_manager.get_service("manabox")
|
||||||
|
|
||||||
success = await manabox_service.process_manabox_csv(db, content, metadata)
|
success = await manabox_service.process_manabox_csv(db, content, metadata, background_tasks)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=400, detail="Failed to process CSV file")
|
raise HTTPException(status_code=400, detail="Failed to process CSV file")
|
||||||
|
@ -220,6 +220,7 @@ async def print_pirate_ship_label(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to print Pirate Ship label: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Failed to print Pirate Ship label: {str(e)}")
|
||||||
|
|
||||||
|
# what even is this TODO delete
|
||||||
@router.post("/process-manabox-csv")
|
@router.post("/process-manabox-csv")
|
||||||
async def process_manabox_csv(
|
async def process_manabox_csv(
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
|
0
app/schemas/inventory.py
Normal file
0
app/schemas/inventory.py
Normal file
@ -1,12 +1,12 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from app.models.tcgplayer_products import TCGPlayerProduct
|
||||||
class PurchaseItem(BaseModel):
|
class PurchaseItem(BaseModel):
|
||||||
product_id: int
|
product_id: int
|
||||||
unit_price: float
|
unit_price: float
|
||||||
quantity: int
|
quantity: int
|
||||||
is_case: bool
|
item_type: str
|
||||||
num_boxes: Optional[int] = None
|
num_boxes: Optional[int] = None
|
||||||
# TODO: remove is_case and num_boxes, should derive from product_id
|
# TODO: remove is_case and num_boxes, should derive from product_id
|
||||||
|
|
||||||
@ -30,11 +30,11 @@ class SaleTransactionCreate(BaseModel):
|
|||||||
class TransactionItemResponse(BaseModel):
|
class TransactionItemResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
transaction_id: int
|
transaction_id: int
|
||||||
physical_item_id: int
|
inventory_item_id: int
|
||||||
unit_price: float
|
unit_price: float
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
deleted_at: Optional[datetime] = None
|
||||||
class TransactionResponse(BaseModel):
|
class TransactionResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
vendor_id: Optional[int] = None
|
vendor_id: Optional[int] = None
|
||||||
@ -46,4 +46,64 @@ class TransactionResponse(BaseModel):
|
|||||||
transaction_notes: Optional[str] = None
|
transaction_notes: Optional[str] = None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
transaction_items: List[TransactionItemResponse]
|
deleted_at: Optional[datetime] = None
|
||||||
|
transaction_items: List[TransactionItemResponse]
|
||||||
|
|
||||||
|
|
||||||
|
class GetAllTransactionsResponse(BaseModel):
|
||||||
|
total: int
|
||||||
|
transactions: List[TransactionResponse]
|
||||||
|
|
||||||
|
class SealedExpectedValueResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
tcgplayer_product_id: int
|
||||||
|
expected_value: float
|
||||||
|
|
||||||
|
class SealedExpectedValueCreate(BaseModel):
|
||||||
|
tcgplayer_product_id: int
|
||||||
|
expected_value: float
|
||||||
|
|
||||||
|
class TCGPlayerProductResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
tcgplayer_product_id: int
|
||||||
|
name: str
|
||||||
|
image_url: str
|
||||||
|
category_id: int
|
||||||
|
group_id: int
|
||||||
|
url: str
|
||||||
|
market_price: float
|
||||||
|
category_name: str
|
||||||
|
group_name: str
|
||||||
|
|
||||||
|
class InventoryItemResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
physical_item_id: int
|
||||||
|
cost_basis: float
|
||||||
|
item_type: str
|
||||||
|
listed_price: Optional[float] = None
|
||||||
|
marketplace_listing_id: Optional[int] = None
|
||||||
|
recommended_price: Optional[float] = None
|
||||||
|
parent_id: Optional[int] = None
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
product: Optional[TCGPlayerProductResponse] = None
|
||||||
|
|
||||||
|
class OpenEventResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
source_item_id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
|
||||||
|
class OpenEventCreate(BaseModel):
|
||||||
|
inventory_item_id: int
|
||||||
|
manabox_file_upload_ids: List[int]
|
||||||
|
|
||||||
|
class OpenEventResultingItemsResponse(BaseModel):
|
||||||
|
id: int
|
||||||
|
source_item_id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: datetime
|
||||||
|
resulting_items: List[InventoryItemResponse]
|
||||||
|
|
||||||
|
class OpenEventsForInventoryItemResponse(BaseModel):
|
||||||
|
open_events: List[OpenEventResponse]
|
||||||
|
@ -772,7 +772,7 @@ class DataInitializationService(BaseService):
|
|||||||
transaction = await inventory_service.create_purchase_transaction(db, PurchaseTransactionCreate(
|
transaction = await inventory_service.create_purchase_transaction(db, PurchaseTransactionCreate(
|
||||||
vendor_id=vendor.id,
|
vendor_id=vendor.id,
|
||||||
transaction_date=datetime.now(),
|
transaction_date=datetime.now(),
|
||||||
items=[PurchaseItem(product_id=619645, unit_price=100, quantity=1, is_case=False)],
|
items=[PurchaseItem(product_id=619645, unit_price=100, quantity=1, item_type="box")],
|
||||||
transaction_notes="tdm real box test"
|
transaction_notes="tdm real box test"
|
||||||
#PurchaseItem(product_id=562119, unit_price=800.01, quantity=2, is_case=True, num_boxes=6)],
|
#PurchaseItem(product_id=562119, unit_price=800.01, quantity=2, is_case=True, num_boxes=6)],
|
||||||
#transaction_notes="Test Transaction: 1 case and 2 boxes of foundations"
|
#transaction_notes="Test Transaction: 1 case and 2 boxes of foundations"
|
||||||
@ -784,7 +784,7 @@ class DataInitializationService(BaseService):
|
|||||||
if item.inventory_item.physical_item.item_type == "box":
|
if item.inventory_item.physical_item.item_type == "box":
|
||||||
manabox_service = self.get_service("manabox")
|
manabox_service = self.get_service("manabox")
|
||||||
#file_path = 'app/data/test_data/manabox_test_file.csv'
|
#file_path = 'app/data/test_data/manabox_test_file.csv'
|
||||||
file_path = 'app/data/test_data/tdmtest.csv'
|
file_path = 'app/data/test_data/dragon.csv'
|
||||||
file_bytes = open(file_path, 'rb').read()
|
file_bytes = open(file_path, 'rb').read()
|
||||||
manabox_file = await manabox_service.process_manabox_csv(db, file_bytes, {"source": "test", "description": "test"}, wait=True)
|
manabox_file = await manabox_service.process_manabox_csv(db, file_bytes, {"source": "test", "description": "test"}, wait=True)
|
||||||
# Ensure manabox_file is a list before passing it
|
# Ensure manabox_file is a list before passing it
|
||||||
|
@ -120,7 +120,7 @@ class FileService:
|
|||||||
"""List files with optional filtering"""
|
"""List files with optional filtering"""
|
||||||
query = db.query(File)
|
query = db.query(File)
|
||||||
if file_type:
|
if file_type:
|
||||||
query = query.filter(File.type == file_type).order_by(File.created_at.desc())
|
query = query.filter(File.file_type == file_type).order_by(File.created_at.desc())
|
||||||
files = query.offset(skip).limit(limit).all()
|
files = query.offset(skip).limit(limit).all()
|
||||||
return [FileInDB.model_validate(file) for file in files]
|
return [FileInDB.model_validate(file) for file in files]
|
||||||
|
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
from typing import List, Optional, Dict, TypedDict
|
from typing import List, Optional, Dict, TypedDict
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session, joinedload
|
||||||
|
from decimal import Decimal
|
||||||
from app.services.base_service import BaseService
|
from app.services.base_service import BaseService
|
||||||
from app.models.manabox_import_staging import ManaboxImportStaging
|
from app.models.manabox_import_staging import ManaboxImportStaging
|
||||||
from app.contexts.inventory_item import InventoryItemContextFactory
|
from app.contexts.inventory_item import InventoryItemContextFactory
|
||||||
from app.models.inventory_management import (
|
from app.models.inventory_management import (
|
||||||
OpenEvent, Card, InventoryItem, Case,
|
OpenEvent, Card, InventoryItem, Case, SealedExpectedValue,
|
||||||
Transaction, TransactionItem, Customer, Vendor, Marketplace, Box, MarketplaceListing
|
Transaction, TransactionItem, Customer, Vendor, Marketplace, Box, MarketplaceListing
|
||||||
)
|
)
|
||||||
from app.schemas.file import FileInDB
|
from app.schemas.file import FileInDB
|
||||||
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, TransactionResponse
|
from app.models.inventory_management import PhysicalItem
|
||||||
|
from app.schemas.transaction import PurchaseTransactionCreate, SaleTransactionCreate, TransactionResponse, SealedExpectedValueCreate
|
||||||
from app.db.database import transaction as db_transaction
|
from app.db.database import transaction as db_transaction
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -20,6 +22,58 @@ class InventoryService(BaseService):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(None)
|
super().__init__(None)
|
||||||
|
|
||||||
|
async def get_resulting_items_for_open_event(self, db: Session, open_event: OpenEvent) -> List[InventoryItem]:
|
||||||
|
# Get the IDs of resulting items
|
||||||
|
resulting_item_ids = [item.id for item in open_event.resulting_items]
|
||||||
|
# Query using the IDs
|
||||||
|
return db.query(InventoryItem).filter(InventoryItem.physical_item_id.in_(resulting_item_ids)).all()
|
||||||
|
|
||||||
|
async def get_open_event(self, db: Session, inventory_item: InventoryItem, open_event_id: int) -> OpenEvent:
|
||||||
|
return db.query(OpenEvent).filter(OpenEvent.source_item == inventory_item.physical_item).filter(OpenEvent.id == open_event_id).first()
|
||||||
|
|
||||||
|
async def get_open_events_for_inventory_item(self, db: Session, inventory_item: InventoryItem) -> List[OpenEvent]:
|
||||||
|
return db.query(OpenEvent).filter(OpenEvent.source_item == inventory_item.physical_item).all()
|
||||||
|
|
||||||
|
async def get_inventory_item(self, db: Session, inventory_item_id: int) -> InventoryItem:
|
||||||
|
return db.query(InventoryItem)\
|
||||||
|
.options(
|
||||||
|
joinedload(InventoryItem.physical_item).joinedload(PhysicalItem.product_direct)
|
||||||
|
)\
|
||||||
|
.filter(InventoryItem.id == inventory_item_id)\
|
||||||
|
.first()
|
||||||
|
|
||||||
|
async def get_expected_value(self, db: Session, product_id: int) -> float:
|
||||||
|
expected_value = db.query(SealedExpectedValue).filter(SealedExpectedValue.tcgplayer_product_id == product_id).first()
|
||||||
|
return expected_value.expected_value if expected_value else None
|
||||||
|
|
||||||
|
async def get_transactions(self, db: Session, skip: int, limit: int) -> List[Transaction]:
|
||||||
|
return db.query(Transaction)\
|
||||||
|
.order_by(Transaction.transaction_date.desc())\
|
||||||
|
.offset(skip)\
|
||||||
|
.limit(limit)\
|
||||||
|
.all()
|
||||||
|
|
||||||
|
async def get_transaction(self, db: Session, transaction_id: int) -> Transaction:
|
||||||
|
return db.query(Transaction)\
|
||||||
|
.options(
|
||||||
|
joinedload(Transaction.transaction_items).joinedload(TransactionItem.inventory_item).joinedload(InventoryItem.physical_item).joinedload(PhysicalItem.product_direct),
|
||||||
|
joinedload(Transaction.vendors),
|
||||||
|
joinedload(Transaction.customers),
|
||||||
|
joinedload(Transaction.marketplaces)
|
||||||
|
)\
|
||||||
|
.filter(Transaction.id == transaction_id)\
|
||||||
|
.first()
|
||||||
|
|
||||||
|
async def create_expected_value(self, db: Session, expected_value_data: SealedExpectedValueCreate) -> SealedExpectedValue:
|
||||||
|
with db_transaction(db):
|
||||||
|
expected_value = SealedExpectedValue(
|
||||||
|
tcgplayer_product_id=expected_value_data.tcgplayer_product_id,
|
||||||
|
expected_value=expected_value_data.expected_value
|
||||||
|
)
|
||||||
|
db.add(expected_value)
|
||||||
|
db.flush()
|
||||||
|
return expected_value
|
||||||
|
|
||||||
async def create_purchase_transaction(
|
async def create_purchase_transaction(
|
||||||
self,
|
self,
|
||||||
db: Session,
|
db: Session,
|
||||||
@ -52,7 +106,7 @@ class InventoryService(BaseService):
|
|||||||
# Create the physical item based on type
|
# Create the physical item based on type
|
||||||
# TODO: remove is_case and num_boxes, should derive from product_id
|
# TODO: remove is_case and num_boxes, should derive from product_id
|
||||||
# TODO: add support for purchasing single cards
|
# TODO: add support for purchasing single cards
|
||||||
if item.is_case:
|
if item.item_type == "case":
|
||||||
for i in range(item.quantity):
|
for i in range(item.quantity):
|
||||||
physical_item = await case_service.create_case(
|
physical_item = await case_service.create_case(
|
||||||
db=db,
|
db=db,
|
||||||
@ -61,7 +115,7 @@ class InventoryService(BaseService):
|
|||||||
num_boxes=item.num_boxes
|
num_boxes=item.num_boxes
|
||||||
)
|
)
|
||||||
physical_items.append(physical_item)
|
physical_items.append(physical_item)
|
||||||
else:
|
elif item.item_type == "box":
|
||||||
for i in range(item.quantity):
|
for i in range(item.quantity):
|
||||||
physical_item = await box_service.create_box(
|
physical_item = await box_service.create_box(
|
||||||
db=db,
|
db=db,
|
||||||
@ -69,6 +123,9 @@ class InventoryService(BaseService):
|
|||||||
cost_basis=item.unit_price
|
cost_basis=item.unit_price
|
||||||
)
|
)
|
||||||
physical_items.append(physical_item)
|
physical_items.append(physical_item)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid item type: {item.item_type}")
|
||||||
|
# TODO: add support for purchasing single cards
|
||||||
|
|
||||||
for physical_item in physical_items:
|
for physical_item in physical_items:
|
||||||
# Create transaction item
|
# Create transaction item
|
||||||
@ -128,6 +185,12 @@ class InventoryService(BaseService):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
async def get_vendors(
|
||||||
|
self,
|
||||||
|
db: Session
|
||||||
|
) -> List[Vendor]:
|
||||||
|
return db.query(Vendor).all()
|
||||||
|
|
||||||
async def create_marketplace(
|
async def create_marketplace(
|
||||||
self,
|
self,
|
||||||
db: Session,
|
db: Session,
|
||||||
@ -149,6 +212,12 @@ class InventoryService(BaseService):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
async def get_marketplaces(
|
||||||
|
self,
|
||||||
|
db: Session
|
||||||
|
) -> List[Marketplace]:
|
||||||
|
return db.query(Marketplace).all()
|
||||||
|
|
||||||
class BoxService(BaseService[Box]):
|
class BoxService(BaseService[Box]):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -329,3 +398,106 @@ class MarketplaceListingService(BaseService[MarketplaceListing]):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
async def get_marketplace_listing(self, db: Session, inventory_item: InventoryItem, marketplace: Marketplace) -> MarketplaceListing:
|
||||||
|
return db.query(MarketplaceListing).filter(MarketplaceListing.inventory_item == inventory_item).filter(MarketplaceListing.delisting_date == None).filter(MarketplaceListing.deleted_at == None).filter(MarketplaceListing.marketplace == marketplace).order_by(MarketplaceListing.created_at.desc()).first()
|
||||||
|
|
||||||
|
async def get_active_marketplace_listing(self, db: Session, inventory_item: InventoryItem, marketplace: Marketplace) -> MarketplaceListing:
|
||||||
|
return db.query(MarketplaceListing).filter(MarketplaceListing.inventory_item == inventory_item).filter(MarketplaceListing.delisting_date == None).filter(MarketplaceListing.deleted_at == None).filter(MarketplaceListing.marketplace == marketplace).filter(MarketplaceListing.listing_date != None).order_by(MarketplaceListing.created_at.desc()).first()
|
||||||
|
|
||||||
|
async def confirm_listings(self, db: Session, open_event: OpenEvent, marketplace: Marketplace) -> str:
|
||||||
|
tcgplayer_add_file = await self.create_tcgplayer_add_file(db, open_event, marketplace)
|
||||||
|
if not tcgplayer_add_file:
|
||||||
|
raise ValueError("No TCGplayer add file created")
|
||||||
|
with db_transaction(db):
|
||||||
|
for resulting_item in open_event.resulting_items:
|
||||||
|
marketplace_listing = await self.get_marketplace_listing(db, resulting_item.inventory_item, marketplace)
|
||||||
|
if marketplace_listing is None:
|
||||||
|
raise ValueError(f"No active marketplace listing found for inventory item id {resulting_item.inventory_item.id} in {marketplace.name}")
|
||||||
|
marketplace_listing.listing_date = datetime.now()
|
||||||
|
db.flush()
|
||||||
|
return tcgplayer_add_file
|
||||||
|
|
||||||
|
async def create_tcgplayer_add_file(self, db: Session, open_event: OpenEvent, marketplace: Marketplace) -> str:
|
||||||
|
# TCGplayer Id,Product Line,Set Name,Product Name,Title,Number,Rarity,Condition,TCG Market Price,TCG Direct Low,TCG Low Price With Shipping,TCG Low Price,Total Quantity,Add to Quantity,TCG Marketplace Price,Photo URL
|
||||||
|
headers = [
|
||||||
|
"TCGplayer Id",
|
||||||
|
"Product Line",
|
||||||
|
"Set Name",
|
||||||
|
"Product Name",
|
||||||
|
"Title",
|
||||||
|
"Number",
|
||||||
|
"Rarity",
|
||||||
|
"Condition",
|
||||||
|
"TCG Market Price",
|
||||||
|
"TCG Direct Low",
|
||||||
|
"TCG Low Price With Shipping",
|
||||||
|
"TCG Low Price",
|
||||||
|
"Total Quantity",
|
||||||
|
"Add to Quantity",
|
||||||
|
"TCG Marketplace Price",
|
||||||
|
"Photo URL"
|
||||||
|
]
|
||||||
|
data = {}
|
||||||
|
for resulting_item in open_event.resulting_items:
|
||||||
|
marketplace_listing = await self.get_marketplace_listing(db, resulting_item.inventory_item, marketplace)
|
||||||
|
if marketplace_listing is None:
|
||||||
|
raise ValueError(f"No active marketplace listing found for inventory item id {resulting_item.inventory_item.id} in {marketplace.name}")
|
||||||
|
tcgplayer_sku_id = resulting_item.tcgplayer_sku_id
|
||||||
|
if tcgplayer_sku_id in data:
|
||||||
|
data[tcgplayer_sku_id]["Add to Quantity"] += 1
|
||||||
|
continue
|
||||||
|
product_line = resulting_item.products.category.name
|
||||||
|
set_name = resulting_item.products.group.name
|
||||||
|
product_name = resulting_item.products.name
|
||||||
|
title = ""
|
||||||
|
number = resulting_item.products.ext_number
|
||||||
|
rarity = resulting_item.products.ext_rarity
|
||||||
|
condition = " ".join([condition.capitalize() for condition in resulting_item.sku.condition.split(" ")]) + (" " + resulting_item.products.sub_type_name if resulting_item.products.sub_type_name == "Foil" else "")
|
||||||
|
tcg_market_price = resulting_item.products.most_recent_tcgplayer_price.market_price
|
||||||
|
tcg_direct_low = resulting_item.products.most_recent_tcgplayer_price.direct_low_price
|
||||||
|
tcg_low_price_with_shipping = resulting_item.products.most_recent_tcgplayer_price.low_price
|
||||||
|
tcg_low_price = resulting_item.products.most_recent_tcgplayer_price.low_price
|
||||||
|
total_quantity = ""
|
||||||
|
add_to_quantity = 1
|
||||||
|
# get average recommended price of product
|
||||||
|
# get inventory items with same tcgplayer_product_id
|
||||||
|
inventory_items = db.query(InventoryItem).filter(InventoryItem.physical_item.has(tcgplayer_sku_id=tcgplayer_sku_id)).all()
|
||||||
|
inventory_item_ids = [inventory_item.id for inventory_item in inventory_items]
|
||||||
|
logger.debug(f"inventory_item_ids: {inventory_item_ids}")
|
||||||
|
valid_listings = db.query(MarketplaceListing).filter(MarketplaceListing.inventory_item_id.in_(inventory_item_ids)).filter(MarketplaceListing.delisting_date == None).filter(MarketplaceListing.deleted_at == None).filter(MarketplaceListing.listing_date == None).all()
|
||||||
|
logger.debug(f"valid_listings: {valid_listings}")
|
||||||
|
avg_recommended_price = sum([listing.recommended_price.price for listing in valid_listings]) / len(valid_listings)
|
||||||
|
data[tcgplayer_sku_id] = {
|
||||||
|
"TCGplayer Id": tcgplayer_sku_id,
|
||||||
|
"Product Line": product_line,
|
||||||
|
"Set Name": set_name,
|
||||||
|
"Product Name": product_name,
|
||||||
|
"Title": title,
|
||||||
|
"Number": number,
|
||||||
|
"Rarity": rarity,
|
||||||
|
"Condition": condition,
|
||||||
|
"TCG Market Price": tcg_market_price,
|
||||||
|
"TCG Direct Low": tcg_direct_low,
|
||||||
|
"TCG Low Price With Shipping": tcg_low_price_with_shipping,
|
||||||
|
"TCG Low Price": tcg_low_price,
|
||||||
|
"Total Quantity": total_quantity,
|
||||||
|
"Add to Quantity": add_to_quantity,
|
||||||
|
"TCG Marketplace Price": f"{Decimal(avg_recommended_price):.2f}",
|
||||||
|
"Photo URL": ""
|
||||||
|
}
|
||||||
|
# format data into csv
|
||||||
|
# header
|
||||||
|
header_row = ",".join(headers)
|
||||||
|
# data
|
||||||
|
def escape_csv_value(value):
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
value = str(value)
|
||||||
|
if any(c in value for c in [',', '"', '\n']):
|
||||||
|
return f'"{value.replace('"', '""')}"'
|
||||||
|
return value
|
||||||
|
|
||||||
|
data_rows = [",".join([escape_csv_value(data[tcgplayer_id][header]) for header in headers]) for tcgplayer_id in data]
|
||||||
|
csv_data = "\n".join([header_row] + data_rows)
|
||||||
|
return csv_data
|
||||||
|
@ -9,7 +9,7 @@ from typing import Dict, Any, Union, List
|
|||||||
import csv
|
import csv
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import asyncio
|
from fastapi import BackgroundTasks
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ class ManaboxService(BaseService):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(None)
|
super().__init__(None)
|
||||||
|
|
||||||
async def process_manabox_csv(self, db: Session, bytes: bytes, metadata: Dict[str, Any], wait: bool = False) -> Union[bool, List[FileInDB]]:
|
async def process_manabox_csv(self, db: Session, bytes: bytes, metadata: Dict[str, Any], background_tasks: BackgroundTasks, wait: bool = False) -> Union[bool, List[FileInDB]]:
|
||||||
# save file
|
# save file
|
||||||
file = await self.file_service.save_file(
|
file = await self.file_service.save_file(
|
||||||
db=db,
|
db=db,
|
||||||
@ -29,34 +29,36 @@ class ManaboxService(BaseService):
|
|||||||
metadata=metadata
|
metadata=metadata
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create the background task
|
|
||||||
task = asyncio.create_task(self._process_file_background(db, file))
|
|
||||||
|
|
||||||
# If wait is True, wait for the task to complete and return the file
|
|
||||||
if wait:
|
if wait:
|
||||||
await task
|
await self._process_file_background(db, file)
|
||||||
return_value = await self.file_service.get_file(db, file.id)
|
return_value = await self.file_service.get_file(db, file.id)
|
||||||
return [return_value] if return_value else []
|
return [return_value] if return_value else []
|
||||||
|
else:
|
||||||
return True
|
background_tasks.add_task(self._process_file_background, db, file)
|
||||||
|
return True
|
||||||
|
|
||||||
async def _process_file_background(self, db: Session, file: FileInDB):
|
async def _process_file_background(self, db: Session, file: FileInDB):
|
||||||
try:
|
try:
|
||||||
# Read the CSV file
|
# Read the CSV file
|
||||||
with open(file.path, 'r') as csv_file:
|
with open(file.path, 'r') as csv_file:
|
||||||
reader = csv.DictReader(csv_file)
|
reader = csv.DictReader(csv_file)
|
||||||
|
logger.debug(f"Processing file: {file.path}")
|
||||||
# Pre-fetch all MTGJSONCards for Scryfall IDs in the file
|
# Pre-fetch all MTGJSONCards for Scryfall IDs in the file
|
||||||
scryfall_ids = {row['Scryfall ID'] for row in reader}
|
scryfall_ids = {row['Scryfall ID'] for row in reader}
|
||||||
mtg_json_map = {card.scryfall_id: card for card in db.query(MTGJSONCard).filter(MTGJSONCard.scryfall_id.in_(scryfall_ids)).all()}
|
mtg_json_map = {card.scryfall_id: card for card in db.query(MTGJSONCard).filter(MTGJSONCard.scryfall_id.in_(scryfall_ids)).all()}
|
||||||
|
logger.debug(f"len ids: {len(scryfall_ids)}")
|
||||||
|
|
||||||
# Re-read the file to process the rows
|
# Re-read the file to process the rows
|
||||||
csv_file.seek(0)
|
csv_file.seek(0)
|
||||||
|
logger.debug(f"header: {reader.fieldnames}")
|
||||||
next(reader) # Skip the header row
|
next(reader) # Skip the header row
|
||||||
|
|
||||||
staging_entries = [] # To collect all staging entries for batch insert
|
staging_entries = [] # To collect all staging entries for batch insert
|
||||||
critical_errors = [] # To collect errors for logging
|
critical_errors = [] # To collect errors for logging
|
||||||
|
|
||||||
for row in reader:
|
for row in reader:
|
||||||
|
|
||||||
|
logger.debug(f"Processing row: {row}")
|
||||||
mtg_json = mtg_json_map.get(row['Scryfall ID'])
|
mtg_json = mtg_json_map.get(row['Scryfall ID'])
|
||||||
|
|
||||||
if not mtg_json:
|
if not mtg_json:
|
||||||
@ -109,6 +111,7 @@ class ManaboxService(BaseService):
|
|||||||
|
|
||||||
# Prepare the staging entry
|
# Prepare the staging entry
|
||||||
quantity = int(row['Quantity'])
|
quantity = int(row['Quantity'])
|
||||||
|
logger.debug(f"inserting row file id: {file.id} tcgplayer_product_id: {tcgplayer_product.tcgplayer_product_id} tcgplayer_sku_id: {tcgplayer_sku.tcgplayer_sku_id} quantity: {quantity}")
|
||||||
staging_entries.append(ManaboxImportStaging(
|
staging_entries.append(ManaboxImportStaging(
|
||||||
file_id=file.id,
|
file_id=file.id,
|
||||||
tcgplayer_product_id=tcgplayer_product.tcgplayer_product_id,
|
tcgplayer_product_id=tcgplayer_product.tcgplayer_product_id,
|
||||||
@ -118,10 +121,13 @@ class ManaboxService(BaseService):
|
|||||||
|
|
||||||
# Bulk insert all valid ManaboxImportStaging entries
|
# Bulk insert all valid ManaboxImportStaging entries
|
||||||
if staging_entries:
|
if staging_entries:
|
||||||
db.bulk_save_objects(staging_entries)
|
logger.debug(f"inserting {len(staging_entries)} rows")
|
||||||
|
with transaction(db):
|
||||||
|
db.bulk_save_objects(staging_entries)
|
||||||
|
|
||||||
# Log any critical errors that occurred
|
# Log any critical errors that occurred
|
||||||
for error_message in critical_errors:
|
for error_message in critical_errors:
|
||||||
|
logger.debug(f"logging critical error: {error_message}")
|
||||||
with transaction(db):
|
with transaction(db):
|
||||||
critical_error_log = CriticalErrorLog(error_message=error_message)
|
critical_error_log = CriticalErrorLog(error_message=error_message)
|
||||||
db.add(critical_error_log)
|
db.add(critical_error_log)
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import logging
|
import logging
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from app.services.base_service import BaseService
|
from app.services.base_service import BaseService
|
||||||
from app.models.inventory_management import InventoryItem
|
from app.models.inventory_management import InventoryItem, MarketplaceListing
|
||||||
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
from app.models.tcgplayer_inventory import TCGPlayerInventory
|
||||||
from app.models.pricing import PricingEvent
|
from app.models.pricing import PricingEvent
|
||||||
from app.db.database import transaction
|
from app.db.database import transaction
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -148,8 +151,32 @@ class PricingService(BaseService):
|
|||||||
free_shipping_adjustment=free_shipping_adjustment
|
free_shipping_adjustment=free_shipping_adjustment
|
||||||
)
|
)
|
||||||
db.add(pricing_event)
|
db.add(pricing_event)
|
||||||
|
|
||||||
|
# delete previous pricing events for inventory item
|
||||||
|
if inventory_item.marketplace_listing and inventory_item.marketplace_listing.listed_price:
|
||||||
|
inventory_item.marketplace_listing.listed_price.deleted_at = datetime.now()
|
||||||
|
|
||||||
return pricing_event
|
return pricing_event
|
||||||
|
|
||||||
def set_price_for_unmanaged_inventory(self, db: Session, tcgplayer_sku_id: int, quantity: int) -> float:
|
def set_price_for_unmanaged_inventory(self, db: Session, tcgplayer_sku_id: int, quantity: int) -> float:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def update_price_for_product(self, db: Session, tcgplayer_sku_id: int) -> list[PricingEvent]:
|
||||||
|
# get inventory items for sku
|
||||||
|
updated_prices = []
|
||||||
|
inventory_items = db.query(InventoryItem).filter(
|
||||||
|
InventoryItem.physical_item.tcgplayer_sku_id == tcgplayer_sku_id
|
||||||
|
).all()
|
||||||
|
for inventory_item in inventory_items:
|
||||||
|
pricing_event = self.set_price(db, inventory_item)
|
||||||
|
updated_prices.append(pricing_event)
|
||||||
|
return updated_prices
|
||||||
|
|
||||||
|
def set_price_for_product(self, db: Session, tcgplayer_sku_id: int) -> float:
|
||||||
|
# update price for all inventory items for sku
|
||||||
|
prices = self.update_price_for_product(db, tcgplayer_sku_id)
|
||||||
|
sum_prices = sum(price.price for price in prices)
|
||||||
|
average_price = sum_prices / len(prices)
|
||||||
|
return average_price
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ import aiohttp
|
|||||||
import jinja2
|
import jinja2
|
||||||
from weasyprint import HTML
|
from weasyprint import HTML
|
||||||
from app.services.base_service import BaseService
|
from app.services.base_service import BaseService
|
||||||
from app.models.tcgplayer_products import TCGPlayerProduct
|
from app.models.tcgplayer_products import TCGPlayerProduct, TCGPlayerGroup
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -20,6 +20,22 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-900 min-h-screen text-gray-100">
|
<body class="bg-gray-900 min-h-screen text-gray-100">
|
||||||
|
<!-- Navigation Menu -->
|
||||||
|
<nav class="bg-gray-800 shadow-sm">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex items-center justify-between h-16">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<a href="/" class="text-xl font-bold text-gray-100">TCGPlayer Manager</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<a href="/" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Orders</a>
|
||||||
|
<a href="/manabox.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Manabox</a>
|
||||||
|
<a href="/transactions.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700">Transactions</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||||
<h1 class="text-3xl font-bold text-gray-100 mb-2">TCGPlayer Order Management</h1>
|
<h1 class="text-3xl font-bold text-gray-100 mb-2">TCGPlayer Order Management</h1>
|
||||||
|
86
app/static/manabox.html
Normal file
86
app/static/manabox.html
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Manabox Inventory Management</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-900 min-h-screen text-gray-100">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-100 mb-2">Manabox Inventory Management</h1>
|
||||||
|
<p class="text-gray-400">Upload and manage your Manabox inventory</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Upload Section -->
|
||||||
|
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-100 mb-6">Upload Manabox CSV</h2>
|
||||||
|
<form id="uploadForm" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="source" class="block text-sm font-medium text-gray-300 mb-2">Source</label>
|
||||||
|
<input type="text" id="source" name="source" required
|
||||||
|
class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="description" class="block text-sm font-medium text-gray-300 mb-2">Description</label>
|
||||||
|
<textarea id="description" name="description" rows="3" required
|
||||||
|
class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500"></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="csvFile" class="block text-sm font-medium text-gray-300 mb-2">CSV File</label>
|
||||||
|
<input type="file" id="csvFile" name="file" accept=".csv" required
|
||||||
|
class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||||
|
Upload CSV
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Uploads List Section -->
|
||||||
|
<div class="bg-gray-800 rounded-xl shadow-sm p-6">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-100">Recent Uploads</h2>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<button onclick="selectAllUploads()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||||
|
Select All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-700">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">
|
||||||
|
<input type="checkbox" id="selectAll" class="rounded border-gray-600 bg-gray-800 text-blue-600 focus:ring-blue-500">
|
||||||
|
</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Source</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Description</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Upload Date</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="uploadsList" class="divide-y divide-gray-700">
|
||||||
|
<!-- Uploads will be populated here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="/manabox.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
170
app/static/manabox.js
Normal file
170
app/static/manabox.js
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
// API base URL
|
||||||
|
const API_BASE_URL = '/api';
|
||||||
|
|
||||||
|
// Selected uploads for actions
|
||||||
|
let selectedUploads = new Set();
|
||||||
|
|
||||||
|
// Show toast notification
|
||||||
|
function showToast(message, type = 'success') {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `fixed bottom-4 right-4 px-6 py-3 rounded-lg shadow-lg text-white ${
|
||||||
|
type === 'success' ? 'bg-green-600' : 'bg-red-600'
|
||||||
|
} transform translate-y-0 opacity-100 transition-all duration-300`;
|
||||||
|
toast.textContent = message;
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.transform = 'translateY(100%)';
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
setTimeout(() => toast.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
function setLoading(isLoading) {
|
||||||
|
const buttons = document.querySelectorAll('button');
|
||||||
|
buttons.forEach(button => {
|
||||||
|
if (isLoading) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.classList.add('opacity-50', 'cursor-not-allowed');
|
||||||
|
} else {
|
||||||
|
button.disabled = false;
|
||||||
|
button.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
document.getElementById('uploadForm').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', document.getElementById('csvFile').files[0]);
|
||||||
|
formData.append('source', document.getElementById('source').value);
|
||||||
|
formData.append('description', document.getElementById('description').value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch(`${API_BASE_URL}/manabox/process-csv`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.detail || 'Failed to upload CSV');
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast('CSV uploaded successfully');
|
||||||
|
document.getElementById('uploadForm').reset();
|
||||||
|
fetchUploads(); // Refresh the uploads list
|
||||||
|
} catch (error) {
|
||||||
|
showToast('Error uploading CSV: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch uploads from the API
|
||||||
|
async function fetchUploads() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch(`${API_BASE_URL}/manabox/manabox-file-uploads`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch uploads');
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploads = await response.json();
|
||||||
|
displayUploads(uploads);
|
||||||
|
} catch (error) {
|
||||||
|
showToast('Error fetching uploads: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display uploads in the UI
|
||||||
|
function displayUploads(uploads) {
|
||||||
|
const uploadsList = document.getElementById('uploadsList');
|
||||||
|
uploadsList.innerHTML = '';
|
||||||
|
|
||||||
|
if (!uploads || uploads.length === 0) {
|
||||||
|
uploadsList.innerHTML = '<tr><td colspan="5" class="px-6 py-4 text-center text-gray-400">No uploads found</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploads.forEach(upload => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.className = 'hover:bg-gray-700';
|
||||||
|
row.dataset.uploadId = upload.id;
|
||||||
|
row.innerHTML = `
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<input type="checkbox" class="upload-checkbox rounded border-gray-600 bg-gray-800 text-blue-600 focus:ring-blue-500">
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${upload.name || 'N/A'}</td>
|
||||||
|
<td class="px-6 py-4 text-sm text-gray-300">${upload.file_metadata?.description || 'N/A'}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-300">${formatDate(upload.created_at)}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span class="px-2 py-1 text-xs rounded-full bg-green-900/50 text-green-300">Processed</span>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
uploadsList.appendChild(row);
|
||||||
|
|
||||||
|
// Add click event listener to the checkbox
|
||||||
|
const checkbox = row.querySelector('.upload-checkbox');
|
||||||
|
checkbox.addEventListener('change', () => {
|
||||||
|
const uploadId = row.dataset.uploadId;
|
||||||
|
if (checkbox.checked) {
|
||||||
|
selectedUploads.add(uploadId);
|
||||||
|
} else {
|
||||||
|
selectedUploads.delete(uploadId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to format date
|
||||||
|
function formatDate(dateString) {
|
||||||
|
if (!dateString) return 'N/A';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select all uploads
|
||||||
|
function selectAllUploads() {
|
||||||
|
const checkboxes = document.querySelectorAll('.upload-checkbox');
|
||||||
|
const allSelected = checkboxes.length > 0 && Array.from(checkboxes).every(checkbox => checkbox.checked);
|
||||||
|
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = !allSelected;
|
||||||
|
const row = checkbox.closest('tr');
|
||||||
|
const uploadId = row.dataset.uploadId;
|
||||||
|
if (!allSelected) {
|
||||||
|
selectedUploads.add(uploadId);
|
||||||
|
} else {
|
||||||
|
selectedUploads.delete(uploadId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
showToast(allSelected ? 'All uploads deselected' : 'All uploads selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the page
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
fetchUploads();
|
||||||
|
|
||||||
|
// Add event listener for the select all checkbox
|
||||||
|
document.getElementById('selectAll').addEventListener('change', (e) => {
|
||||||
|
const checkboxes = document.querySelectorAll('.upload-checkbox');
|
||||||
|
checkboxes.forEach(checkbox => {
|
||||||
|
checkbox.checked = e.target.checked;
|
||||||
|
const row = checkbox.closest('tr');
|
||||||
|
const uploadId = row.dataset.uploadId;
|
||||||
|
if (e.target.checked) {
|
||||||
|
selectedUploads.add(uploadId);
|
||||||
|
} else {
|
||||||
|
selectedUploads.delete(uploadId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -105,4 +105,126 @@ button:hover {
|
|||||||
select, button {
|
select, button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transaction Page Styles */
|
||||||
|
.transaction-form {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-form .form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-form label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-form .form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-form .btn-add {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-form .items-section {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-list {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card {
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card h3 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-card p {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal-content {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Support */
|
||||||
|
body.dark-mode {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .container {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .transaction-card {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .transaction-card h3 {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .transaction-card p {
|
||||||
|
color: #b3b3b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal-content {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal-header,
|
||||||
|
body.dark-mode .modal-footer {
|
||||||
|
border-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .form-control {
|
||||||
|
background-color: #404040;
|
||||||
|
border-color: #505050;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .form-control:focus {
|
||||||
|
background-color: #404040;
|
||||||
|
border-color: #007bff;
|
||||||
|
color: #ffffff;
|
||||||
}
|
}
|
179
app/static/transactions.html
Normal file
179
app/static/transactions.html
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Transactions - AI Giga TCG</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', 'sans-serif'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-900 min-h-screen text-gray-100">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-100 mb-2">Transactions</h1>
|
||||||
|
<p class="text-gray-400">Manage your transactions</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Transaction Button -->
|
||||||
|
<div class="bg-gray-800 rounded-xl shadow-sm p-6 mb-8">
|
||||||
|
<button id="createTransactionBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||||
|
Create New Transaction
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transaction List -->
|
||||||
|
<div id="transactionList" class="bg-gray-800 rounded-xl shadow-sm p-6">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-100">Recent Transactions</h2>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<label for="limitSelect" class="text-sm text-gray-300">Show:</label>
|
||||||
|
<select id="limitSelect" class="rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="transactionsTable" class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-700">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Date</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Type</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Vendor/Customer</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Total</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-300 uppercase tracking-wider">Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-700" id="transactionsBody">
|
||||||
|
<!-- Transactions will be loaded here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- Pagination Controls -->
|
||||||
|
<div class="flex justify-between items-center mt-4">
|
||||||
|
<button id="prevPageBtn" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<span id="pageInfo" class="text-gray-300">Page 1</span>
|
||||||
|
<button id="nextPageBtn" class="px-4 py-2 bg-gray-700 text-white rounded-lg hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Create Transaction Modal -->
|
||||||
|
<div id="createTransactionModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center">
|
||||||
|
<div class="bg-gray-800 rounded-lg p-6 max-w-2xl w-full mx-4">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-xl font-semibold text-gray-100">Create Transaction</h3>
|
||||||
|
<button onclick="closeTransactionModal()" class="text-gray-400 hover:text-white">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="transactionForm" class="space-y-4">
|
||||||
|
<!-- Transaction Type -->
|
||||||
|
<div>
|
||||||
|
<label for="transactionType" class="block text-sm font-medium text-gray-300 mb-2">Transaction Type</label>
|
||||||
|
<select id="transactionType" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
<option value="purchase" selected>Purchase</option>
|
||||||
|
<option value="sale">Sale</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vendor/Customer Selection -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<label for="vendorSelect" class="block text-sm font-medium text-gray-300">Vendor</label>
|
||||||
|
<button type="button" id="addVendorBtn" class="px-2 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||||
|
Add New
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<select id="vendorSelect" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
<option value="">Select a vendor</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Marketplace Selection (for sales) -->
|
||||||
|
<div id="marketplaceSection" class="hidden">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<label for="marketplaceSelect" class="block text-sm font-medium text-gray-300">Marketplace</label>
|
||||||
|
<button type="button" id="addMarketplaceBtn" class="px-2 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||||
|
Add New
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<select id="marketplaceSelect" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500">
|
||||||
|
<option value="">Select a marketplace</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transaction Date -->
|
||||||
|
<div>
|
||||||
|
<label for="transactionDate" class="block text-sm font-medium text-gray-300 mb-2">Transaction Date</label>
|
||||||
|
<input type="datetime-local" id="transactionDate" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transaction Notes -->
|
||||||
|
<div>
|
||||||
|
<label for="transactionNotes" class="block text-sm font-medium text-gray-300 mb-2">Notes</label>
|
||||||
|
<textarea id="transactionNotes" class="w-full rounded-lg border-gray-600 bg-gray-700 text-gray-100 focus:ring-blue-500 focus:border-blue-500" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items Section -->
|
||||||
|
<div id="itemsSection" class="border border-gray-700 rounded-lg p-4">
|
||||||
|
<h5 class="text-lg font-medium text-gray-100 mb-4">Items</h5>
|
||||||
|
<div id="itemsContainer" class="space-y-4">
|
||||||
|
<!-- Items will be added here -->
|
||||||
|
</div>
|
||||||
|
<button type="button" id="addItemBtn" class="mt-4 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors">
|
||||||
|
Add Item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class="flex justify-end space-x-3 mt-6">
|
||||||
|
<button onclick="closeTransactionModal()" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button id="saveTransactionBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
|
||||||
|
Save Transaction
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Transaction Details Modal -->
|
||||||
|
<div id="transactionDetailsModal" class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center">
|
||||||
|
<div class="bg-gray-800 rounded-lg p-6 max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h3 class="text-xl font-semibold text-gray-100">Transaction Details</h3>
|
||||||
|
<button onclick="closeTransactionDetailsModal()" class="text-gray-400 hover:text-white">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="transactionDetails" class="space-y-4">
|
||||||
|
<!-- Transaction details will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="transactions.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
1077
app/static/transactions.js
Normal file
1077
app/static/transactions.js
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user