Система интернет-магазина существует потому, что местный продавец, ведущий магазин в переписке, рано или поздно теряет заказ. Замена переписке нарочито неэффектная: каталог с поиском и фильтром категорий, страница товара, показывающая остаток до нажатия, корзина, про которую всегда понятно, к какому магазину она относится, и заказ, проходящий пять названных состояний с уведомлением на каждом переходе.
Страница товара: варианты, остатки и скидка до нажатия
У каждого товара есть фотографии, описание, остаток и цена, которую можно снизить: цена со скидкой стоит рядом с зачёркнутой исходной и значком процента, так что размер скидки читается, а не подразумевается. Если товара нет, это нарисовано поверх фотографии, а не обнаруживается при оформлении. Это следует правилу, на котором построено всё приложение: всё, что меняет смысл нажатия, должно быть видно до нажатия. Кнопка не должна обещать того, в чём откажет следующий экран.
Варианты порождаются из атрибутов, а не заводятся по одному. Продавец объявляет оси — размер, цвет, что бы там ни менялось, — и сочетания становятся вариантами, у каждого свой остаток. Так в данные не попадает типичная порча: три отдельно набранные карточки одной рубашки, по которым нельзя построить общий отчёт, и остаток, верный ровно для одной из них.
Корзина принадлежит одному магазину
Корзина — одного магазина. Добавление товара другого продавца спрашивает, очистить ли то, что уже лежит, вместо того чтобы тихо собирать корзину из четырёх заведений. Это отказ по замыслу, а не отсутствующая функция: корзина из нескольких магазинов подразумевает одно оформление, один расчёт доставки и одну точку ответственности, когда что-то не приехало, — а ничего из этого не существует, когда продавцы — четыре независимых местных бизнеса с четырьмя разными договорённостями о доставке.
Один магазин на корзину, намеренно. Смена магазина предлагает очистить корзину. Корзина из нескольких продавцов, чтобы быть честной, требует единого оформления, а сквозного маркетплейс-оформления здесь нет: каждый заказ — между покупателем и одним заведением.
Стоимость доставки не приходит полем из приложения. Её читают из записи самого магазина на сервере в момент оформления, и она применяется только к доставке.
Оформление: доставка или самовывоз и цена, которую клиент не задаёт
Оформление спрашивает то, что заказу действительно нужно: доставка или самовывоз, имя, телефон, адрес, комментарий и способ оплаты. Выбор самовывоза убирает стоимость доставки совсем, а не показывает ноль, потому что это два разных состояния, а ноль читается как ошибка. Адреса берутся из адресной книги с адресом по умолчанию, так что второй заказ не требует перенабирать адрес первого, — и книга одна, общая с профилем, потому что адрес в профиле и адрес доставки — один и тот же факт, а хранение его дважды гарантирует, что копии однажды разойдутся.
Сама стоимость доставки живёт на сервере. place_shop_order читает businesses.delivery_fee из базы и игнорирует то, что прислал клиент, ровно как путь обслуживания за столом сам считает каждую строку. Поле клиента, которому доверяют, — скидка себе самому; деньгами распоряжается сервер. Это не настройка, которую можно выключить, — это место, откуда берётся число.
Пять состояний заказа, трек-номер, обе стороны уведомлены
Заказ проходит «ожидает», «подтверждён», «упаковывается», «отправлен» и «доставлен», а «отменён» и «отклонён» — конечные ветки. Покупатель видит полосу прогресса, а не отдельно стоящее слово, поэтому «упаковывается» читается как позиция в последовательности, за которой что-то следует. На шаге отправки прикрепляется трек-номер вместе с перевозчиком, выбранным из списка, который ведёт администрация: у каждого перевозчика есть шаблон ссылки на отслеживание, поэтому номер, который вводит магазин, превращается в ссылку, открываемую покупателем. Прикрепить трек-номер, не указав перевозчика, нельзя: номер, о котором неизвестно, чей он, ссылкой никогда не станет.
У обоих окончаний есть причина. Покупатель, отменяющий заказ, обязан её указать, и магазин, отклоняющий заказ, тоже, — и причина едет вместе с заказом, а не остаётся сообщением, уехавшим вверх по переписке. Каждый переход уведомляет обе стороны, потому что при заказах без уведомлений магазины пропускают заказы, а покупатели остаются в догадках.
- ожидает — оформлен, ждёт магазин
- подтверждён — принят, магазин взялся
- упаковывается — собирается
- отправлен — в пути, с трек-номером и перевозчиком
- доставлен — закрыт, и покупатель получает право на отзыв
Сторона продавца: касса, онлайн-заказы и остатки
Розничный бизнес получает три плитки консоли: кассу для продаж в зале, консоль онлайн-заказов и товары со складом и предупреждениями о низком остатке. На панели живые значки обновляются каждые несколько секунд, так что новый заказ заявляет о себе сам, а не ждёт, пока его найдут. Продажа в зале и продажа через приложение пишутся в один журнал.
Раз оба канала попадают в одно место, отчёты покрывают весь бизнес, а не онлайновый кусок: выручка, число заказов, средний чек, разбивка на наличные, PromptPay и прочее, пятёрка лидеров продаж, столбчатый график выручки за семь дней и периоды «сегодня», «7 дней», «30 дней», «всё время». Чеки можно перепечатать, а из той же записи собрать текст чека для отправки. Налоговую накладную выставить нельзя: это доступно только плательщикам НДС, а регистрация ни одного заведения в системе пока не записана. Список клиентов строится из того же журнала — с заметками и метками, историей покупок, баллами, которые копятся по ставке, заданной самим заведением в своей валюте, с настраиваемым минимальным чеком и наградой.
Доступ персонала управляется матрицей прав с ролевыми пресетами и точечными исключениями, так что продавцу можно дать товары и заказы без зарплат и отчётов. Та же матрица есть в базе данных как has_capability(business_id, capability), которая раскрывает пресеты на сервере для политик защиты на уровне строк, — сервер не верит клиенту на слово о том, какая у кого роль.
Рядом живут два родственных реестра с той же логикой учёта. То, чем заведение владеет, но не торгует — инструмент, техника, мебель, — лежит в реестре активов, а то, что оно выдаёт под залог, проходит через прокат.
Отзыв о товаре пишет только тот, кто его купил
Отзывы на товары отделены от отзыва о заведении, и оба ограничены одинаково: написать может только покупатель, про которого платформа может доказать сделку. Право определяется представлением в базе, и покупка — один из подходящих видов наряду с обедом в зале, доставкой, проживанием и полученной услугой. Все остальные видят карточку с замком, объясняющую, почему кнопки нет, вместо кнопки, которая откажет.
Рейтинг — не хранимое число, которое кто-то правит. Триггер базы пересчитывает его из строк отзывов — тот же механизм подробно разобран на странице доверия, — поэтому оценка на карточке магазина и отзывы на странице не могут разойтись. Покупатель может отредактировать свой последний отзыв, удалить его или добавить новый при повторной покупке, а старый останется; магазин отвечает публично.