{"openapi":"3.0.3","info":{"title":"ALDI Hackathon API","version":"1.0.0","description":"Products, categories, recipes, stores and 9x9 store-grid layouts for the ALDI Hackathon. Includes a route-planner that returns the shortest in-store path to collect every ingredient of a recipe and finish at the checkout.","contact":{"name":"ALDI Hackathon"}},"servers":[{"url":"/","description":"This server"}],"tags":[{"name":"Categories"},{"name":"Products"},{"name":"Recipes"},{"name":"Stores"}],"paths":{"/api/categories":{"get":{"tags":["Categories"],"summary":"List all categories","responses":{"200":{"description":"All product categories","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"categories":{"type":"array","items":{"$ref":"#/components/schemas/Category"}}}}}}}}}},"/api/products":{"get":{"tags":["Products"],"summary":"List / filter products","parameters":[{"name":"category_id","in":"query","schema":{"type":"integer"},"description":"Filter by category id"},{"name":"ingredient_key","in":"query","schema":{"type":"string"},"description":"Filter by ingredient grouping key"},{"name":"q","in":"query","schema":{"type":"string"},"description":"Case-insensitive name search"},{"name":"max_price","in":"query","schema":{"type":"number"},"description":"Maximum price (EUR)"},{"name":"sort","in":"query","schema":{"type":"string","enum":["price","wholesale_price","margin"]}}],"responses":{"200":{"description":"Matching products","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"products":{"type":"array","items":{"$ref":"#/components/schemas/Product"}}}}}}}}}},"/api/recipes":{"get":{"tags":["Recipes"],"summary":"List / search recipes","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Search name/description/ingredient"},{"name":"tag","in":"query","schema":{"type":"string"},"description":"Filter by tag, e.g. vegetarian"}],"responses":{"200":{"description":"Matching recipes","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"recipes":{"type":"array","items":{"$ref":"#/components/schemas/Recipe"}}}}}}}}}},"/api/recipes/{id}":{"get":{"tags":["Recipes"],"summary":"Recipe detail with ALDI product options","description":"Returns each ingredient with multiple ALDI product options (size & price), the cheapest and the ALDI-profit-maximising pick, scaled to the requested number of portions.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"portions","in":"query","schema":{"type":"integer"},"description":"Scale amounts to this many portions"},{"name":"exclude_pantry","in":"query","schema":{"type":"boolean"},"description":"Drop common household staples from the basket"}],"responses":{"200":{"description":"Recipe with product options & basket summary"},"404":{"description":"Recipe not found"}}}},"/api/stores":{"get":{"tags":["Stores"],"summary":"List all stores","responses":{"200":{"description":"All stores","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"stores":{"type":"array","items":{"$ref":"#/components/schemas/Store"}}}}}}}}}},"/api/stores/{id}":{"get":{"tags":["Stores"],"summary":"Store detail","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Store"},"404":{"description":"Not found"}}}},"/api/stores/{id}/grid":{"get":{"tags":["Stores"],"summary":"9x9 store grid layout","description":"Returns the 9x9 grid. Each cell has x/y coordinates and the food categories stocked there. One cell is the checkout, one is the entrance; every store contains all 16 categories.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Store grid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoreGrid"}}}},"404":{"description":"Not found"}}}},"/api/stores/{id}/route-plan":{"get":{"tags":["Stores"],"summary":"Optimised in-store shopping route","description":"Computes the shortest route (entrance → category cells → checkout) using a nearest-neighbour heuristic over Manhattan distance. Supply categories directly or derive them from recipes.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"categories","in":"query","schema":{"type":"string"},"description":"Comma-separated category ids, e.g. 1,3,9"},{"name":"recipe_id","in":"query","schema":{"type":"integer"},"description":"Derive categories from a recipe"},{"name":"recipe_ids","in":"query","schema":{"type":"string"},"description":"Comma-separated recipe ids"},{"name":"exclude_pantry","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Route plan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoutePlan"}}}},"400":{"description":"No categories provided"},"404":{"description":"Store not found"}}}},"/api/openapi":{"get":{"tags":["Stores"],"summary":"This OpenAPI document (JSON)","responses":{"200":{"description":"OpenAPI 3.0 document"}}}}},"components":{"schemas":{"Category":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"slug":{"type":"string"}}},"Product":{"type":"object","properties":{"id":{"type":"integer"},"category_id":{"type":"integer"},"category":{"type":"string"},"name":{"type":"string"},"price":{"type":"number"},"wholesale_price":{"type":"number"},"size":{"type":"string"},"unit":{"type":"string"},"unit_amount":{"type":"number"},"ingredient_key":{"type":"string"}}},"RecipeIngredient":{"type":"object","properties":{"ingredient_key":{"type":"string"},"name":{"type":"string"},"amount":{"type":"number"},"unit":{"type":"string"},"category_id":{"type":"integer"},"category":{"type":"string"},"pantry_staple":{"type":"boolean"}}},"Recipe":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"description":{"type":"string"},"cuisine":{"type":"string"},"base_portions":{"type":"integer"},"prep_minutes":{"type":"integer"},"tags":{"type":"array","items":{"type":"string"}},"ingredients":{"type":"array","items":{"$ref":"#/components/schemas/RecipeIngredient"}}}},"Store":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"city":{"type":"string"},"address":{"type":"string"},"lat":{"type":"number"},"lng":{"type":"number"},"grid_size":{"type":"integer"}}},"GridCell":{"type":"object","properties":{"x":{"type":"integer"},"y":{"type":"integer"},"type":{"type":"string","enum":["aisle","checkout","entrance"]},"category_ids":{"type":"array","items":{"type":"integer"}},"categories":{"type":"array","items":{"type":"string"}},"label":{"type":"string"}}},"StoreGrid":{"type":"object","properties":{"store_id":{"type":"integer"},"store_name":{"type":"string"},"width":{"type":"integer"},"height":{"type":"integer"},"entrance":{"type":"object","properties":{"x":{"type":"integer"},"y":{"type":"integer"}}},"checkout":{"type":"object","properties":{"x":{"type":"integer"},"y":{"type":"integer"}}},"cells":{"type":"array","items":{"$ref":"#/components/schemas/GridCell"}}}},"RoutePlan":{"type":"object","properties":{"store_id":{"type":"integer"},"store_name":{"type":"string"},"required_category_ids":{"type":"array","items":{"type":"integer"}},"unavailable_category_ids":{"type":"array","items":{"type":"integer"}},"total_steps":{"type":"integer"},"stops":{"type":"array","items":{"type":"object","properties":{"order":{"type":"integer"},"x":{"type":"integer"},"y":{"type":"integer"},"category_id":{"type":"integer","nullable":true},"category":{"type":"string"},"label":{"type":"string"},"steps_from_previous":{"type":"integer"}}}},"path":{"type":"array","items":{"type":"object","properties":{"x":{"type":"integer"},"y":{"type":"integer"}}}}}}}}}