Skip to content

Latest commit

 

History

History
213 lines (162 loc) · 15.5 KB

TASK.md

File metadata and controls

213 lines (162 loc) · 15.5 KB

Задача: CRUD и корзина для интернет-магазина

Легенда

Наше хранилище данных уже умеет выдавать уникальные идентификаторы, но пока это лишь «голая» база: записи лежат в памяти, и работать с ними приходится вручную. Чтобы проверить, что хранилище годится для настоящей прикладной задачи, мы построим на нём серверную часть простого интернет-магазина: каталог товаров и корзину покупателя.

Если писать такой код «в лоб», быстро возникают проблемы:

  • записи о товарах разбросаны по программе, и каждый кусок кода сам решает, как их создавать, менять и удалять, — в итоге в каталоге появляются дубликаты и «осиротевшие» идентификаторы;
  • деньги хранят во float, и суммы вроде 0.1 + 0.2 начинают расходиться с копейками, а в бухгалтерии такое недопустимо;
  • товар «переезжает» между складом и корзиной в два отдельных действия, и если одно из них не выполнилось, товар пропадает или появляется из воздуха.

Именно эти вопросы решают классические CRUD-операции (create, read, update, delete) над единым хранилищем, аккуратная работа с ценой через Decimal и операции корзины, которые сохраняют общее количество товара. Их нам и предстоит написать. Позже на этих функциях мы построим интерфейс работы с нашей базой.

Контекст

Нужно реализовать серверную часть простого интернет-магазина: хранилище товаров, операции CRUD над ним и работу с корзиной покупателя. Настоящая база данных не используется — хранилище это список кортежей в оперативной памяти.

Решение состоит из четырёх модулей:

  • storage.py — форма записи о товаре и связанные с ней константы;
  • utils.py — округление денежных сумм;
  • crud.py — создание, чтение, изменение и удаление записей о товарах;
  • cart.py — перенос товаров между хранилищем и корзиной.

Все функции, кроме find_cart_line и change_cart_quantity, на неуспешном пути ничего не бросают: печатают сообщение об ошибке в stdout и возвращают None.

Формат записи о товаре

Запись о товаре (Product) — кортеж из четырёх полей, перечисленных по порядку:

Поле Тип Что хранит
идентификатор int уникальный положительный ключ, назначается при создании записи
наименование str человекочитаемое имя, уникальное в пределах хранилища
цена Decimal цена за единицу, округлённая до фиксированного числа знаков дроби
количество int число единиц товара на складе

Позиции полей задаются константами *_INDEX, чтобы обращение к кортежу не использовало числовые литералы.

Строка корзины (CartLine) — кортеж (идентификатор товара, количество). В корзине не более одной строки на каждый товар.

Модуль storage.py

Определите:

  • Product — псевдоним типа tuple[int, str, Decimal, int].
  • PRODUCT_ID_INDEX, NAME_INDEX, PRICE_INDEX, QUANTITY_INDEX — позиции полей в кортеже Product: 0, 1, 2, 3. Каждая аннотирована typing.Final.
  • PRODUCT_ID_MIN: Final[int] = 1 — идентификатор, который получает первый товар, добавленный в пустое хранилище.

Модуль utils.py

  • PRICE_PRECISION: Final[int] = 2 — число знаков после запятой, до которого округляется каждая сохраняемая цена.
  • PRICE_STEP: Final — шаг квантования. Вычисляется из PRICE_PRECISION (Decimal(1).scaleb(-PRICE_PRECISION), то есть Decimal("0.01") при значении по умолчанию), а не задаётся числом напрямую; при изменении PRICE_PRECISION шаг меняется согласованно.

normalize_price(price: Decimal) -> Decimal

  • Вход: price — произвольная денежная сумма типа Decimal.
  • Выход: та же сумма, приведённая к PRICE_PRECISION знакам после запятой; ровно половина шага округляется вверх (режим ROUND_HALF_UP).

Примеры: Decimal("12.999") → Decimal("13.00"), Decimal("2.3451") → Decimal("2.35"), Decimal("2.3449") → Decimal("2.34"), Decimal("5") → Decimal("5.00") (число знаков дроби в результате всегда равно PRICE_PRECISION).

Модуль crud.py

Каждая функция получает хранилище storage: list[Product] первым параметром и меняет его на месте.

generate_product_id(storage: list[Product]) -> int

  • Вход: хранилище.
  • Выход: если хранилище пустое — PRODUCT_ID_MIN; иначе — наибольший идентификатор среди записей плюс 1. Пропуски в нумерации (например, оставшиеся после удаления) не переиспользуются — берётся именно максимум.

create_product(storage: list[Product], fields: tuple[str, Decimal, int]) -> int | None

  • Вход: fields — кортеж (наименование, цена, количество); цена типа Decimal.
  • Выход: сгенерированный идентификатор новой записи, либо None.

Поведение.

  1. Если в хранилище уже есть запись с таким же наименованием — новая запись не создаётся, хранилище не меняется, печатается сообщение об ошибке, содержащее это наименование, возвращается None. (Например: product name '<имя>' is already taken.)
  2. Иначе идентификатор вычисляется через generate_product_id, цена приводится через normalize_price, кортеж (идентификатор, наименование, нормализованная цена, количество) добавляется в хранилище, возвращается идентификатор.

read_product(storage: list[Product], product_id: int) -> Product | None

  • Вход: целочисленный идентификатор.
  • Выход: запись с этим идентификатором, либо None. Если записи нет — печатается сообщение об ошибке, содержащее переданный идентификатор (например, no product with id <id>).

update_product(storage: list[Product], product_id: int, fields: tuple[str, Decimal, int]) -> Product | None

  • Вход: идентификатор и кортеж (наименование, цена, количество) с новыми значениями полей.
  • Выход: изменённая запись (идентификатор, наименование, нормализованная цена, количество) — сам идентификатор сохраняется прежним, остальные три поля заменяются, цена приводится через normalize_price. Если записи с таким идентификатором нет — хранилище не меняется, печатается сообщение об ошибке с идентификатором (no product with id <id>), возвращается None. Уникальность нового наименования не проверяется.

delete_product(storage: list[Product], product_id: int) -> int | None

  • Вход: идентификатор.
  • Выход: тот же идентификатор, если запись была найдена и удалена из хранилища. Если записи нет — хранилище не меняется, печатается сообщение об ошибке с идентификатором (no product with id <id>), возвращается None.

Модуль cart.py

  • CartLine — псевдоним типа tuple[int, int] ((идентификатор товара, количество)).
  • LINE_PRODUCT_ID_INDEX, LINE_QUANTITY_INDEX — позиции полей строки корзины (0, 1), аннотированы typing.Final.

add_to_cart(storage: list[Product], cart: list[CartLine], product_id: int, quantity: int) -> CartLine | None

Переносит quantity единиц товара product_id со склада в корзину.

  • Вход: хранилище, корзина, идентификатор товара, количество для добавления.
  • Выход: строка корзины для product_id после добавления, либо None.

Поведение. Проверки выполняются в этом порядке; при первой же неудаче остальные действия не выполняются:

  1. Товар ищется в хранилище (через read_product). Если его нет — read_product печатает сообщение с идентификатором, add_to_cart возвращает None, больше ничего не меняется.
  2. Иначе количество на складе сравнивается с запрошенным. Если на складе строго меньше, чем quantity — печатается сообщение об ошибке, содержащее идентификатор товара (например: not enough stock for product <id>: <склад> available, <quantity> requested), возвращается None, ничего не меняется. Запрос ровно на всё имеющееся количество (quantity == склад) допустим.
  3. Иначе количество товара в хранилище уменьшается на quantity (наименование и цена не меняются), а в корзину добавляется quantity единиц: если строка для товара уже была — увеличивается её количество, иначе создаётся новая строка. Возвращается получившаяся строка корзины.

remove_from_cart(storage: list[Product], cart: list[CartLine], product_id: int, quantity: int) -> CartLine | None

Переносит quantity единиц товара product_id из корзины обратно на склад. Принимает тот же набор аргументов, что и add_to_cart.

  • Выход: строка корзины для product_id после удаления (количество 0 означает, что строка удалена), либо None.

Поведение. Проверки в этом порядке:

  1. Строка корзины для product_id ищется через find_cart_line. Если её нет — печатается сообщение об ошибке, содержащее идентификатор (например: product <id> is not in the cart), возвращается None, ничего не меняется.
  2. Иначе количество в строке корзины сравнивается с запрошенным. Если в корзине строго меньше, чем quantity — печатается сообщение об ошибке с идентификатором (например: cart holds only <в корзине> unit(s) of product <id>, cannot remove <quantity>), возвращается None, ничего не меняется.
  3. Товар ищется в хранилище через read_product; если его там нет — возвращается None (сообщение печатает read_product).
  4. Иначе количество товара в хранилище увеличивается на quantity, а из строки корзины вычитается quantity (наименование и цена не меняются); если количество в строке стало 0, строка удаляется. Возвращается получившаяся строка корзины.

Инварианты

  • Для любой успешно созданной записи read_product(storage, create_product(storage, (name, price, qty))) возвращает (id, name, normalize_price(price), qty).
  • После delete_product(storage, id) вызов read_product(storage, id) возвращает None.
  • Цена в хранилище всегда нормализована: normalize_price применяется при каждом создании и изменении записи.
  • Сохранение количества: при успешном add_to_cart или remove_from_cart сумма «количество товара на складе + количество этого товара в корзине» не меняется.
  • add_to_cart(...) и следующий за ним remove_from_cart(...) с тем же quantity возвращают склад и корзину к исходному состоянию.