{
  "openapi": "3.1.1",
  "info": {
    "title": "Webion.Patty.Api | v1",
    "description": "",
    "version": "1.0"
  },
  "servers": [
    {
      "url": "http://patty-api-staging.webion.it/"
    }
  ],
  "paths": {
    "/api/v1/menu/categories": {
      "get": {
        "tags": [
          "Menu"
        ],
        "summary": "Lists the categories of the menu with the number of items in each. Reads the menu page only,\nnever the detail pages.",
        "responses": {
          "200": {
            "description": "The categories.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ListMenuCategoriesResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListMenuCategoriesResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListMenuCategoriesResponse"
                }
              }
            }
          },
          "502": {
            "description": "pattyburger.it could not be read."
          }
        }
      }
    },
    "/api/v1/menu/{slug}": {
      "get": {
        "tags": [
          "Menu"
        ],
        "summary": "Reads a single item of the menu, always with its ingredients and allergens.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The item's identifier, for example `original-patty`.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "refresh",
            "in": "query",
            "description": "Ignores the cached scrape and reads the site again.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The item.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/MenuItemDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MenuItemDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/MenuItemDto"
                }
              }
            }
          },
          "404": {
            "description": "The menu holds no item with that slug.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "502": {
            "description": "pattyburger.it could not be read."
          }
        }
      }
    },
    "/api/v1/menu": {
      "get": {
        "tags": [
          "Menu"
        ],
        "summary": "Reads the whole menu from pattyburger.it: name, photo and price of every item and, unless\nasked otherwise, the ingredients and allergens from each item's own page.",
        "parameters": [
          {
            "name": "includeDetails",
            "in": "query",
            "description": "Whether every item's detail page is read as well, which is what fills in details. Defaults to true; false answers from the menu page alone and is much faster.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Returns only the items of this category, compared case-insensitively.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "refresh",
            "in": "query",
            "description": "Ignores the cached scrape and reads the site again.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The menu, in the order the site lists it.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/GetMenuResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetMenuResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetMenuResponse"
                }
              }
            }
          },
          "502": {
            "description": "pattyburger.it could not be read."
          }
        }
      }
    },
    "/api/v1/game/scores": {
      "post": {
        "tags": [
          "Game"
        ],
        "summary": "Records the score of a run and, when it beats it, replaces the player's best.",
        "description": "The session has to be one this API opened for the caller, still unspent and unexpired, and\nthe score has to be one the game could have produced in the time the session was open. Read\nthe leaderboard afterwards to see where the score landed.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitScoreRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitScoreRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/SubmitScoreRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "The score was recorded."
          },
          "400": {
            "description": "The session is unknown, expired or already spent, or the score is not\none that run could have produced. The cases are deliberately indistinguishable.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "No usable token was presented.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          { }
        ]
      }
    },
    "/api/v1/game/sessions": {
      "post": {
        "tags": [
          "Game"
        ],
        "summary": "Opens a run of the minigame. Call it when the game starts, not when it ends.",
        "description": "The score submitted afterwards has to name the session this returns, and each one buys\nexactly one score. The interval between the two calls is what the server measures the score\nagainst, so a session opened at the end of a game will not buy the score that game produced.",
        "responses": {
          "200": {
            "description": "The session is open.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/StartGameSessionResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StartGameSessionResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/StartGameSessionResponse"
                }
              }
            }
          },
          "401": {
            "description": "No usable token was presented.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too many sessions were opened in the last hour.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          { }
        ]
      }
    },
    "/api/v1/game/leaderboard": {
      "get": {
        "tags": [
          "Game"
        ],
        "summary": "The best players, and the caller's own position.",
        "description": "Open to anyone: without a token it answers the board and nothing else. With one it also fills\n`me`, so a screen showing ten names can still tell the eleventh where they stand.",
        "parameters": [
          {
            "name": "top",
            "in": "query",
            "description": "How many players to return, from the first. Defaults to 10 and is capped by the server.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The leaderboard.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/GetLeaderboardResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetLeaderboardResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetLeaderboardResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/game/coupons/scan": {
      "post": {
        "tags": [
          "Game"
        ],
        "summary": "Spends a coupon from what the till scanned. Only a till account can call it.",
        "description": "The scanned string carries the coupon's code and the proof it was showing at that moment. The\nproof is checked against the ten-second window it belongs to, so a screenshot taken earlier\n— or forwarded to somebody else — no longer matches by the time it is presented.\nSpending is once and for all: the same coupon scanned twice is a 409 the second time.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScanCouponRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ScanCouponRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ScanCouponRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Spent. The discount to apply is in the answer.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ScanCouponResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanCouponResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScanCouponResponse"
                }
              }
            }
          },
          "400": {
            "description": "Nothing usable was scanned.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "No usable token was presented.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The account is not a till.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No coupon carries that code.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The coupon is not spendable, or the code on screen is no longer the\ncurrent one. The two are deliberately indistinguishable.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          { }
        ]
      }
    },
    "/api/v1/game/coupons/current/proof": {
      "get": {
        "tags": [
          "Game"
        ],
        "summary": "The code to show at the till, valid for ten seconds.",
        "description": "Call it again at `expiresAt` while the screen is open: the answer changes every window,\nand a code from a minute ago is refused. The coupon has to be active — a locked one has\nnothing to show and an expired one nothing to spend.",
        "responses": {
          "200": {
            "description": "The current proof.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/GetCouponProofResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetCouponProofResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetCouponProofResponse"
                }
              }
            }
          },
          "401": {
            "description": "No usable token was presented.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "This account holds no coupon.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The coupon is locked, expired or already spent.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          { }
        ]
      }
    },
    "/api/v1/game/coupons/current": {
      "get": {
        "tags": [
          "Game"
        ],
        "summary": "The caller's coupon — the screen that says \"ritira il tuo coupon\".",
        "description": "The most recent one, whatever became of it: a player holds at most one at a time, so the\nlatest is the one there is. It comes back redeemed or expired as well as live, because a\nscreen that has to explain why there is nothing to spend needs to know which of the two\nhappened. The code is only in the answer while the coupon is active.",
        "responses": {
          "200": {
            "description": "The coupon.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/CouponDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CouponDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/CouponDto"
                }
              }
            }
          },
          "401": {
            "description": "No usable token was presented.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "This account has never won one.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          { }
        ]
      }
    },
    "/api/v1/game/coupons/current/activation": {
      "post": {
        "tags": [
          "Game"
        ],
        "summary": "Reveals the coupon and starts its clock. This is what unlocks the code.",
        "description": "    From here the coupon lasts `Game:Coupon:ActiveDuration` and then cannot be spent, so it\n    is worth calling when the customer is at the till rather than when they win it.\n    Calling it again on an already active coupon answers the same one and **does not push the\nexpiry out**: otherwise the window would be as long as somebody cared to keep asking.",
        "responses": {
          "200": {
            "description": "The coupon, now carrying its code.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/CouponDto"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CouponDto"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/CouponDto"
                }
              }
            }
          },
          "401": {
            "description": "No usable token was presented.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "This account holds no coupon.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The coupon has already been spent, or was left to expire.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          { }
        ]
      }
    },
    "/api/v1/episodes": {
      "get": {
        "tags": [
          "Episodes"
        ],
        "summary": "Lists the episodes of \"Smash the rules\" from pattyburger.it: title, video and where the site\nlinks to watch it.",
        "parameters": [
          {
            "name": "refresh",
            "in": "query",
            "description": "Ignores the cached scrape and reads the site again.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The episodes.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ListEpisodesResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListEpisodesResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListEpisodesResponse"
                }
              }
            }
          },
          "502": {
            "description": "pattyburger.it could not be read."
          }
        }
      }
    },
    "/api/v1/auth/register": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Creates an account.",
        "description": "Does not sign the new account in — call sign in afterwards to obtain tokens.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/RegisterRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The account was created.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/RegisterResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegisterResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegisterResponse"
                }
              }
            }
          },
          "400": {
            "description": "The password does not satisfy the policy, or a field is malformed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "An account with that e-mail already exists.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/auth/refresh": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Trades a refresh token for a new pair.",
        "description": "The token sent is spent by this call, so a client must replace it with the one returned.\nPresenting it again shortly afterwards still works — two clients refreshing together both\nhold the old secret — but a later replay is treated as theft and drops every token of the\naccount. Access tokens issued earlier stay valid until they expire.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RefreshTokenRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/RefreshTokenRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/RefreshTokenRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "A new pair was issued.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/AuthTokensResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthTokensResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthTokensResponse"
                }
              }
            }
          },
          "401": {
            "description": "The refresh token is unknown, expired or replayed, or the account is\n            locked out.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/auth/password": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Replaces the caller's password.",
        "description": "Drops every refresh token of the account, so sessions elsewhere cannot be renewed. Access\ntokens already issued keep working until they expire.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangePasswordRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangePasswordRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ChangePasswordRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "The password was changed."
          },
          "400": {
            "description": "The current password is wrong, or the new one does not satisfy the policy.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "No usable token was presented.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          { }
        ]
      }
    },
    "/api/v1/auth/password/forgot": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "E-mails the link that lets the holder of an address choose a new password.",
        "description": "            Answers the same way whether or not an account exists with that address, and says nothing\n            about whether the message went out: either would turn this endpoint into a way of asking\n            which of a list of addresses is registered here. Somebody who has not received the e-mail\n            asks for another.\n            The link carries a token bound to the account's current password, so asking twice invalidates\nnothing — but using one link does invalidate every other, because resetting changes what they\nwere all bound to.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ForgotPasswordRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ForgotPasswordRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ForgotPasswordRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "The request was taken. This is the only answer there is."
          },
          "400": {
            "description": "The address is malformed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/auth/password/reset": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Spends the link from the e-mail and replaces the account's password.",
        "description": "Signs the account out everywhere — every access and refresh token it holds is revoked —\nbecause somebody resetting a password they no longer know may be taking the account back from\nwhoever does. It also clears a lockout: five failed attempts are the ordinary reason to\narrive here, and a reset that left the account locked would look like a reset that did not\nwork.\nA spent link cannot be spent twice: the token is bound to the password it was minted against.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResetPasswordRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ResetPasswordRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ResetPasswordRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "The password was replaced."
          },
          "400": {
            "description": "The link is malformed, expired or already spent, or the new password\n            does not satisfy the policy. The cases are deliberately indistinguishable.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/auth/me/deletion": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Deletes the caller's account, and everything the account owns.",
        "description": "Irreversible, and it takes the scores, the leaderboard entry, the coupons and the tokens with\nit: the account disappears from the board rather than staying there without a name, and the\ntokens stop working at once because there is no longer an account behind them. The current\npassword is required — a token on a phone somebody else picked up is not enough to erase an\naccount.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeleteAccountRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/DeleteAccountRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/DeleteAccountRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "The account, and everything belonging to it, is gone."
          },
          "400": {
            "description": "The password is wrong or missing.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "No usable token was presented.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The account is a till. Tills are made and unmade by hand.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          { }
        ]
      }
    },
    "/api/v1/auth/me": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "The account the presented token belongs to — what the profile page shows.",
        "responses": {
          "200": {
            "description": "The caller's account.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/GetMeResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetMeResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/GetMeResponse"
                }
              }
            }
          },
          "401": {
            "description": "No usable token was presented.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "The token is valid but the account no longer exists.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          { }
        ]
      },
      "put": {
        "tags": [
          "Auth"
        ],
        "summary": "Replaces the caller's profile — the three fields the account is made of.",
        "description": "The e-mail is also the user name, so both are moved together and the next sign in uses the\nnew address. Tokens already issued keep working: the account is the same one.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateMeRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateMeRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateMeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "204": {
            "description": "The profile was updated."
          },
          "400": {
            "description": "A field is malformed.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "401": {
            "description": "No usable token was presented.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Another account already uses that e-mail.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          { }
        ]
      }
    },
    "/api/v1/auth/logout": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Signs the caller out everywhere.",
        "description": "Revokes every access token issued to the caller, not only the one presented, and drops all\ntheir refresh tokens. Tokens on other devices stop working immediately.",
        "responses": {
          "204": {
            "description": "The caller is signed out."
          },
          "401": {
            "description": "No usable token was presented.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        },
        "security": [
          { }
        ]
      }
    },
    "/api/v1/auth/login": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Exchanges e-mail and password for a pair of tokens.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The credentials were accepted.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/AuthTokensResponse"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthTokensResponse"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthTokensResponse"
                }
              }
            }
          },
          "401": {
            "description": "The credentials were rejected, or the account is locked out. The two are\n            deliberately indistinguishable.",
            "content": {
              "text/plain": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              },
              "text/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AccountRole": {
        "enum": [
          "Customer",
          "Till"
        ]
      },
      "AllergenDto": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "iconUrl": {
            "type": [
              "null",
              "string"
            ]
          }
        },
        "description": "An allergen contained in the product, with the icon the site draws for it."
      },
      "AuthTokensResponse": {
        "required": [
          "accessToken",
          "refreshToken",
          "expiresAt"
        ],
        "type": "object",
        "properties": {
          "accessToken": {
            "type": "string",
            "description": "The JWT to send as `Authorization: Bearer …`."
          },
          "refreshToken": {
            "type": "string",
            "description": "Buys the next pair once, and once only: refreshing invalidates it."
          },
          "expiresAt": {
            "type": "string",
            "description": "When the access token stops being accepted. The refresh token outlives it.",
            "format": "date-time"
          }
        },
        "description": "A freshly issued pair of tokens."
      },
      "ChangePasswordRequest": {
        "type": "object",
        "properties": {
          "currentPassword": {
            "type": "string"
          },
          "newPassword": {
            "type": "string",
            "description": "At least 8 characters."
          }
        }
      },
      "CouponDto": {
        "required": [
          "id",
          "status",
          "discountPercentage",
          "grantedAt"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "$ref": "#/components/schemas/CouponStatus"
          },
          "discountPercentage": {
            "type": "integer",
            "format": "int32"
          },
          "grantedAt": {
            "type": "string",
            "format": "date-time"
          },
          "activatedAt": {
            "type": [
              "null",
              "string"
            ],
            "description": "When the holder revealed it, which is when the clock started. Null while locked.",
            "format": "date-time"
          },
          "expiresAt": {
            "type": [
              "null",
              "string"
            ],
            "description": "When it stops being spendable. Null while locked.",
            "format": "date-time"
          },
          "redeemedAt": {
            "type": [
              "null",
              "string"
            ],
            "format": "date-time"
          }
        },
        "description": "A discount won at the minigame."
      },
      "CouponStatus": {
        "enum": [
          "Locked",
          "Active",
          "Expired",
          "Redeemed"
        ],
        "description": "Where a coupon is in its life. Derived from its dates, never stored."
      },
      "DeleteAccountRequest": {
        "type": "object",
        "properties": {
          "password": {
            "type": "string",
            "description": "The caller's current password, which is what confirms the deletion."
          }
        }
      },
      "EpisodeDto": {
        "required": [
          "slug",
          "number",
          "title",
          "actionLabel"
        ],
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "Stable identifier, built from the title — for example `dreaming`."
          },
          "number": {
            "type": "integer",
            "description": "Position in the series, starting at 1.",
            "format": "int32"
          },
          "title": {
            "type": "string",
            "description": "The title as the site writes it, in capitals — `DREAMING`."
          },
          "actionLabel": {
            "type": "string",
            "description": "The label on the site's button, for example `Guarda episodio 1`."
          },
          "videoUrl": {
            "type": [
              "null",
              "string"
            ],
            "description": "The video itself, an MP4 the site plays muted and looping. Null when the slide carries no\nplayable source."
          },
          "watchUrl": {
            "type": [
              "null",
              "string"
            ],
            "description": "Where the site's button goes — the episode on Instagram. Null when the slide has no link."
          }
        },
        "description": "One episode of \"Smash the rules\", the series the site plays on its about page."
      },
      "ForgotPasswordRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "description": "The address the account was registered with. Nothing in the answer says whether it belongs to\none."
          }
        }
      },
      "GetCouponProofResponse": {
        "required": [
          "proof",
          "payload",
          "expiresAt"
        ],
        "type": "object",
        "properties": {
          "proof": {
            "type": "string",
            "description": "The part that rotates. Valid for ten seconds, which is what makes a screenshot useless."
          },
          "payload": {
            "type": "string",
            "description": "Exactly what the QR should carry, so the app does not have to know how the two are joined."
          },
          "expiresAt": {
            "type": "string",
            "description": "When this proof stops being the current one — the moment to ask for the next.",
            "format": "date-time"
          }
        },
        "description": "What the coupon shows at the till right now."
      },
      "GetLeaderboardResponse": {
        "required": [
          "top",
          "playerCount"
        ],
        "type": "object",
        "properties": {
          "top": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LeaderboardEntryDto"
            },
            "description": "The first players, best score first."
          },
          "me": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "description": "The caller's own row, wherever they are — which is what lets a screen show \"you are 27th\"\nunder a list of ten. Null when the request carries no token, or when the caller has never\nsubmitted a score. Present, and repeated, when the caller is already in `top`.",
                "$ref": "#/components/schemas/LeaderboardEntryDto"
              }
            ],
            "description": "The caller's own row, wherever they are — which is what lets a screen show \"you are 27th\"\nunder a list of ten. Null when the request carries no token, or when the caller has never\nsubmitted a score. Present, and repeated, when the caller is already in `top`."
          },
          "playerCount": {
            "type": "integer",
            "description": "How many players have a score at all, which is what a rank is out of.",
            "format": "int32"
          }
        }
      },
      "GetMenuResponse": {
        "required": [
          "source",
          "scrapedAt",
          "count",
          "items"
        ],
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "description": "The page the menu was read from."
          },
          "scrapedAt": {
            "type": "string",
            "description": "When the site was last read. A response served from the cache keeps the timestamp of the\nread it came from, so it does not move until the cache expires.",
            "format": "date-time"
          },
          "count": {
            "type": "integer",
            "description": "How many items `items` holds, after the category filter.",
            "format": "int32"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MenuItemDto"
            }
          }
        }
      },
      "GetMeResponse": {
        "required": [
          "id",
          "email",
          "firstName",
          "lastName",
          "role",
          "createdAt"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string"
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "role": {
            "description": "What the account is allowed to do. `Till` is the one that can spend other people's\ncoupons, and it is what the app branches on to show the scanner instead of the game.",
            "$ref": "#/components/schemas/AccountRole"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastAccessAt": {
            "type": [
              "null",
              "string"
            ],
            "description": "When the account last signed in. Sign in stamps it before issuing the tokens, so on a fresh\nsession this is that sign in and not the one before it. Null only until the first one.",
            "format": "date-time"
          }
        },
        "description": "The profile behind the presented token."
      },
      "IngredientDto": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "iconUrl": {
            "type": [
              "null",
              "string"
            ]
          }
        },
        "description": "An ingredient, with the icon the site draws next to it."
      },
      "LeaderboardEntryDto": {
        "required": [
          "rank",
          "userId",
          "firstName",
          "lastName",
          "points",
          "achievedAt"
        ],
        "type": "object",
        "properties": {
          "rank": {
            "type": "integer",
            "description": "Position, starting at 1. Two players never share one: the earlier best ranks higher.",
            "format": "int32"
          },
          "userId": {
            "type": "string",
            "format": "uuid"
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "points": {
            "type": "integer",
            "format": "int32"
          },
          "achievedAt": {
            "type": "string",
            "description": "When this player reached the score.",
            "format": "date-time"
          }
        },
        "description": "One player on the leaderboard."
      },
      "ListEpisodesResponse": {
        "required": [
          "source",
          "scrapedAt",
          "count",
          "episodes"
        ],
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "description": "The page the episodes were read from."
          },
          "scrapedAt": {
            "type": "string",
            "description": "When the site was last read. A response served from the cache keeps the timestamp of the\nread it came from, so it does not move until the cache expires.",
            "format": "date-time"
          },
          "count": {
            "type": "integer",
            "format": "int32"
          },
          "episodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/EpisodeDto"
            },
            "description": "The episodes in the order the carousel plays them."
          }
        }
      },
      "ListMenuCategoriesResponse": {
        "required": [
          "categories"
        ],
        "type": "object",
        "properties": {
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MenuCategoryDto"
            },
            "description": "The categories, in the order the menu page lists them."
          }
        }
      },
      "LoginRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string"
          },
          "password": {
            "type": "string"
          }
        }
      },
      "MenuCategoryDto": {
        "required": [
          "category",
          "count"
        ],
        "type": "object",
        "properties": {
          "category": {
            "type": "string"
          },
          "count": {
            "type": "integer",
            "format": "int32"
          }
        },
        "description": "A category of the menu and how many items it holds."
      },
      "MenuItemDetailsDto": {
        "required": [
          "descriptionParagraphs",
          "ingredients",
          "allergens"
        ],
        "type": "object",
        "properties": {
          "description": {
            "type": [
              "null",
              "string"
            ],
            "description": "The copy the site publishes for the item, as plain text. Paragraphs are separated by a blank\nline. Use it where the formatting does not matter; `descriptionParagraphs` carries the\nsame words with the emphasis the site gives them."
          },
          "descriptionParagraphs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TextParagraphDto"
            },
            "description": "The same copy, paragraph by paragraph, each split into runs that say whether the site sets\nthem bold, italic, in the brand colour or at another size."
          },
          "ingredients": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IngredientDto"
            }
          },
          "allergens": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AllergenDto"
            },
            "description": "Only the allergens the product actually contains — the site's grid lists all fourteen and\nmarks the ones that apply."
          },
          "allergensNote": {
            "type": [
              "null",
              "string"
            ],
            "description": "The disclaimer the site prints above its allergen grid."
          }
        },
        "description": "What the item's own page adds to the menu listing."
      },
      "MenuItemDto": {
        "required": [
          "slug",
          "name",
          "category"
        ],
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "Stable identifier, taken from the item's page on the site — for example `original-patty`."
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "description": "One of `Panini`, `Patatine`, `Dolci`, `Salse`."
          },
          "priceText": {
            "type": [
              "null",
              "string"
            ],
            "description": "The price exactly as printed on the site, for example `11,90€`.\nNull when the site shows no price for the item."
          },
          "price": {
            "type": [
              "null",
              "number"
            ],
            "description": "The same price as a number, null when `priceText` could not be parsed.",
            "format": "double"
          },
          "currency": {
            "type": "string"
          },
          "photoUrl": {
            "type": [
              "null",
              "string"
            ],
            "description": "Photo of the product, already an absolute URL."
          },
          "titleImageUrl": {
            "type": [
              "null",
              "string"
            ],
            "description": "The image the site uses to write the item's name, since the name itself is drawn, not typed."
          },
          "detailUrl": {
            "type": [
              "null",
              "string"
            ],
            "description": "The item's own page on pattyburger.it. Null for the sauces, which have none."
          },
          "details": {
            "oneOf": [
              {
                "type": "null"
              },
              {
                "description": "Description, ingredients and allergens. Null when the item has no detail page, when details\nwere not requested, or when its page could not be read.",
                "$ref": "#/components/schemas/MenuItemDetailsDto"
              }
            ],
            "description": "Description, ingredients and allergens. Null when the item has no detail page, when details\nwere not requested, or when its page could not be read."
          }
        },
        "description": "One entry of the menu: a burger, a side, a dessert or a sauce."
      },
      "ProblemDetails": {
        "type": "object",
        "properties": {
          "type": {
            "type": [
              "null",
              "string"
            ]
          },
          "title": {
            "type": [
              "null",
              "string"
            ]
          },
          "status": {
            "type": [
              "null",
              "integer"
            ],
            "format": "int32"
          },
          "detail": {
            "type": [
              "null",
              "string"
            ]
          },
          "instance": {
            "type": [
              "null",
              "string"
            ]
          }
        }
      },
      "RefreshTokenRequest": {
        "type": "object",
        "properties": {
          "refreshToken": {
            "type": "string",
            "description": "The refresh token from the last pair issued."
          }
        }
      },
      "RegisterRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string"
          },
          "password": {
            "type": "string",
            "description": "At least 8 characters. No other constraint on the alphabet."
          },
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          }
        }
      },
      "RegisterResponse": {
        "required": [
          "id"
        ],
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The account that was created.",
            "format": "uuid"
          }
        }
      },
      "ResetPasswordRequest": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "description": "The `userId` from the link in the e-mail."
          },
          "token": {
            "type": "string",
            "description": "The `token` from the link in the e-mail, exactly as it was received."
          },
          "newPassword": {
            "type": "string",
            "description": "At least 8 characters."
          }
        }
      },
      "ScanCouponRequest": {
        "type": "object",
        "properties": {
          "scanned": {
            "type": "string",
            "description": "Exactly what the scanner read off the customer's screen, separator included."
          }
        }
      },
      "ScanCouponResponse": {
        "required": [
          "discountPercentage",
          "firstName",
          "redeemedAt"
        ],
        "type": "object",
        "properties": {
          "discountPercentage": {
            "type": "integer",
            "description": "What to take off the bill.",
            "format": "int32"
          },
          "firstName": {
            "type": "string",
            "description": "Whose coupon it was, so the person at the counter can say the name out loud. First name only:\nthe till has no business with the rest of the account."
          },
          "redeemedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "description": "What the till needs to see after a scan that worked."
      },
      "StartGameSessionResponse": {
        "required": [
          "sessionId",
          "expiresAt"
        ],
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "description": "The session to send back with the score. Single use.",
            "format": "uuid"
          },
          "expiresAt": {
            "type": "string",
            "description": "After this instant the session no longer buys a score and a new one has to be opened.",
            "format": "date-time"
          }
        }
      },
      "SubmitScoreRequest": {
        "type": "object",
        "properties": {
          "sessionId": {
            "type": "string",
            "description": "The session the run was played in, from `POST game/sessions`.",
            "format": "uuid"
          },
          "points": {
            "type": "integer",
            "description": "The score the run produced.",
            "format": "int32"
          }
        }
      },
      "TextParagraphDto": {
        "required": [
          "runs"
        ],
        "type": "object",
        "properties": {
          "runs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TextRunDto"
            }
          }
        },
        "description": "One paragraph of the description, split into the runs that are set differently."
      },
      "TextRunDto": {
        "required": [
          "text"
        ],
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "description": "The text. A single line break inside a paragraph survives as `\\n`."
          },
          "bold": {
            "type": "boolean"
          },
          "italic": {
            "type": "boolean"
          },
          "accent": {
            "type": "boolean",
            "description": "The run is set in the brand colour rather than in the body colour."
          },
          "scale": {
            "type": [
              "null",
              "number"
            ],
            "description": "How much larger or smaller than the paragraph the run is set, `1` being the paragraph's\nown size. Null when the site does not resize it.",
            "format": "double"
          }
        },
        "description": "A stretch of text that is set the same way throughout."
      },
      "UpdateMeRequest": {
        "type": "object",
        "properties": {
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "description": "The account's e-mail, which is also what it signs in with. Changing it takes effect at once:\nthe next sign in uses the new address."
          }
        }
      }
    },
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "description": "The access token from sign in, sent as `Authorization: Bearer …`.",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    }
  },
  "tags": [
    {
      "name": "Menu"
    },
    {
      "name": "Game"
    },
    {
      "name": "Episodes"
    },
    {
      "name": "Auth"
    }
  ]
}