Где хранить цену товара, чтобы уже оформленный заказ не менялся после изменения цены в каталоге?

Где хранить цену товара, чтобы уже оформленный заказ не менялся после изменения цены в каталоге?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Цена на момент покупки должна храниться в строке товара заказа, то есть в сущности позиции заказа, как зафиксированное значение. Текущую цену каталога нельзя использовать для отображения исторического заказа: после её изменения сумма старого заказа станет недостоверной.

Исторический контекст

В реляционных системах важно отличать текущие свойства справочника от фактов хозяйственной операции. Цена товара в каталоге описывает актуальное предложение, а цена в оформленном заказе — условие уже совершённой операции.

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

Постановка проблемы

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

Дублирование цены в позиции заказа не является нарушением нормализации, если это не текущий атрибут товара, а цена конкретной продажи. Функциональная зависимость здесь иная: цена продажи определяется оформленной позицией или конкретной операцией, а не только идентификатором товара.

Подробное решение

В позиции заказа следует хранить как минимум ссылку на товар и зафиксированную цену за единицу. Обычно также фиксируют количество, валюту и применённую скидку, если эти значения нужны для воспроизведения расчёта.

CREATE TABLE order_item ( order_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity NUMERIC(12, 3) NOT NULL, unit_price NUMERIC(12, 2) NOT NULL, currency CHAR(3) NOT NULL, PRIMARY KEY (order_id, product_id), FOREIGN KEY (order_id) REFERENCES orders(order_id), FOREIGN KEY (product_id) REFERENCES product(product_id) );

Поле unit_price — это снимок цены, а не дублирование текущего состояния каталога. Связь с товаром всё равно нужна: она сохраняет происхождение позиции и позволяет получать описание товара.

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

Удаление товара из каталога также не должно уничтожать исторические позиции. Обычно товар деактивируют, запрещая новые продажи, либо запрещают его удаление внешним ключом; физическое удаление допустимо только при отдельной стратегии сохранения исторического снимка.

Ситуация из практики

Интернет-магазин сначала хранил в заказе только product_id и количество, а сумму вычислял по таблице каталога. После массового изменения цен служба поддержки обнаружила, что старые заказы в личном кабинете показывают новые суммы, хотя платежи уже были проведены.

Рассматривались три варианта. Пересчитывать старые заказы по архиву цен сложно и требует надёжной временной модели; хранить только ссылку на прайс-лист недостаточно, если правила и цены могут измениться; хранить цену в позиции заказа проще и позволяет воспроизвести документ.

Выбрали снимок цены в order_item, а для аудита дополнительно сохранили идентификатор применённого прайс-листа. В результате изменение каталога влияло только на новые заказы, а исторические суммы оставались неизменными и проверяемыми.

Что кандидаты часто упускают

  1. Разве хранение цены в позиции заказа не нарушает третью нормальную форму?

Нет, если речь идёт о цене продажи на момент операции. Текущая цена товара может зависеть от product_id, но зафиксированная цена зависит от конкретной позиции заказа, выбранного тарифа, времени, клиента или скидки. Удалять такое поле ради формальной ликвидации дублирования нельзя: это уничтожит исторический факт.

  1. Нужно ли хранить в заказе название товара вместе с ценой?

Только если требуется сохранить вид документа на момент покупки или юридически значимое описание. Для обычного каталожного отображения название можно получать по внешнему ключу, но изменение названия тогда изменит представление старого заказа. Если историческое название важно, его также сохраняют как снимок в позиции заказа.

  1. Что делать, если цена изменилась между добавлением товара в корзину и оплатой?

Нужно явно определить момент фиксации цены: например, подтверждение заказа или успешная авторизация платежа. До этого момента система может перепроверять и обновлять цену, но при изменении должна уведомить пользователя или запросить подтверждение. После фиксации заказа цена должна быть неизменяемой либо изменяться только через оформленную корректировку или возврат.