Разработка калькулятора доставки на сайте

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка калькулятора доставки на сайте
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка калькулятора доставки на сайте

Калькулятор доставки должен давать точный ответ до оформления заказа — не «от 300 рублей», а конкретную сумму с выбором способа и датой. Размытые формулировки в корзине — прямая причина отказов. По данным Baymard Institute, неожиданная стоимость доставки на checkout — причина брошенной корзины в 48% случаев.

Архитектура

Запрос (город + товары + вес) → DeliveryCalculator → [Carrier APIs] → Список вариантов → Фронтенд

Калькулятор должен работать как на странице товара (приблизительно), так и в корзине (точно, с учётом всего состава).

Схема данных

CREATE TABLE delivery_zones (
    id          BIGSERIAL PRIMARY KEY,
    name        VARCHAR(255),
    country     CHAR(2) DEFAULT 'RU',
    regions     TEXT[],                    -- коды регионов ФИАС
    cities      TEXT[],                    -- коды городов КЛАДР
    carrier_id  INT REFERENCES carriers(id)
);

CREATE TABLE delivery_rates (
    id              BIGSERIAL PRIMARY KEY,
    carrier_id      INT REFERENCES carriers(id),
    zone_id         BIGINT REFERENCES delivery_zones(id),
    method          VARCHAR(50),           -- 'courier', 'pickup', 'post'
    weight_from_g   INT DEFAULT 0,
    weight_to_g     INT,
    price           NUMERIC(10,2) NOT NULL,
    days_min        SMALLINT,
    days_max        SMALLINT,
    free_from       NUMERIC(12,2),         -- бесплатно при сумме от X
    is_active       BOOLEAN DEFAULT TRUE
);

Сервис вычисления

class DeliveryCalculatorService
{
    public function calculate(DeliveryRequest $request): DeliveryResult
    {
        $totalWeight = $this->calculateWeight($request->items);
        $totalPrice  = $request->items->sum(fn($i) => $i->price * $i->quantity);

        $zone = $this->zoneResolver->resolve($request->destination);

        if (!$zone) {
            // Регион не в таблице зон — считать через API перевозчика
            return $this->calculateViaApi($request, $totalWeight, $totalPrice);
        }

        $rates = DeliveryRate::where('zone_id', $zone->id)
            ->where('weight_from_g', '<=', $totalWeight)
            ->where(fn($q) => $q->whereNull('weight_to_g')->orWhere('weight_to_g', '>=', $totalWeight))
            ->where('is_active', true)
            ->with('carrier')
            ->orderBy('price')
            ->get();

        $options = $rates->map(function ($rate) use ($totalPrice) {
            $price = ($rate->free_from && $totalPrice >= $rate->free_from) ? 0 : $rate->price;

            return new DeliveryOption(
                carrierId:  $rate->carrier_id,
                method:     $rate->method,
                name:       $rate->carrier->name . ' — ' . $rate->method_label,
                price:      $price,
                daysMin:    $rate->days_min,
                daysMax:    $rate->days_max,
                isFree:     $price === 0.0,
            );
        });

        return new DeliveryResult(options: $options, destination: $request->destination);
    }

    private function calculateWeight(Collection $items): int
    {
        return $items->sum(function ($item) {
            $product = $item->product;
            $weight  = $product->weight_g ?? 500; // дефолт 500г если не указан
            return $weight * $item->quantity;
        });
    }
}

Интеграция с СДЭК API

class CdekDeliveryProvider implements DeliveryProviderInterface
{
    private string $baseUrl = 'https://api.cdek.ru/v2';

    public function calculate(DeliveryRequest $request, int $weightG): array
    {
        $token = $this->getToken();

        $response = Http::withToken($token)
            ->post("{$this->baseUrl}/calculator/tarifflist", [
                'type'          => 1,  // 1 = интернет-магазин
                'currency'      => 1,  // RUB
                'lang'          => 'rus',
                'from_location' => ['code' => config('cdek.from_city_code')],
                'to_location'   => ['address' => $request->destination->address],
                'packages'      => [[
                    'weight' => $weightG,
                    'length' => 30,
                    'width'  => 20,
                    'height' => 10,
                ]],
            ]);

        if (!$response->successful()) return [];

        return collect($response->json('tariff_codes', []))
            ->map(fn($t) => new DeliveryOption(
                carrierId:  'cdek',
                method:     $this->mapTariffToMethod($t['tariff_code']),
                name:       'СДЭК — ' . $t['tariff_name'],
                price:      $t['delivery_sum'],
                daysMin:    $t['period_min'],
                daysMax:    $t['period_max'],
            ))
            ->toArray();
    }

    private function getToken(): string
    {
        return Cache::remember('cdek_token', 3600, function () {
            $response = Http::post("{$this->baseUrl}/oauth/token", [
                'grant_type'    => 'client_credentials',
                'client_id'     => config('cdek.client_id'),
                'client_secret' => config('cdek.client_secret'),
            ]);
            return $response->json('access_token');
        });
    }
}

Интеграция с «Почтой России»

class RussianPostProvider implements DeliveryProviderInterface
{
    public function calculate(DeliveryRequest $request, int $weightG): array
    {
        $response = Http::withHeaders([
            'Authorization' => 'AccessToken ' . config('russianpost.token'),
            'X-User-Login'  => config('russianpost.login'),
            'Content-Type'  => 'application/json',
        ])->post('https://tariff.pochta.ru/v2/calculate/tariff', [
            'object'       => 47020,  // ПОСЫЛКА ОНЛАЙН
            'from'         => config('russianpost.from_index'),
            'to'           => $request->destination->postalCode,
            'weight'       => $weightG,
            'fragile'      => false,
            'declared'     => (int) ($request->declaredValue * 100), // в копейках
        ]);

        if (!$response->successful()) return [];

        $total = $response->json('paymoney', 0) / 100;

        return [new DeliveryOption(
            carrierId: 'russianpost',
            method:    'post',
            name:      'Почта России',
            price:     $total,
            daysMin:   $response->json('delivery.min') ?? 7,
            daysMax:   $response->json('delivery.max') ?? 14,
        )];
    }
}

API-эндпоинт для фронтенда

class DeliveryCalculatorController extends Controller
{
    public function calculate(CalculateRequest $request): JsonResponse
    {
        $cacheKey = 'delivery.' . md5(serialize($request->validated()));

        $result = Cache::remember($cacheKey, 300, function () use ($request) {
            return $this->calculator->calculate(
                DeliveryRequest::fromArray($request->validated())
            );
        });

        return response()->json([
            'options'     => DeliveryOptionResource::collection($result->options),
            'destination' => $result->destination->label,
        ]);
    }
}

Фронтенд-компонент

const DeliveryCalculator: React.FC<{ cartItems: CartItem[] }> = ({ cartItems }) => {
  const [city, setCity]       = useState('');
  const [options, setOptions] = useState<DeliveryOption[]>([]);
  const [loading, setLoading] = useState(false);

  const calculate = useDebouncedCallback(async (cityValue: string) => {
    if (cityValue.length < 3) return;
    setLoading(true);
    try {
      const res = await api.post('/delivery/calculate', {
        destination: cityValue,
        items: cartItems.map(i => ({ product_id: i.id, quantity: i.qty })),
      });
      setOptions(res.data.options);
    } finally {
      setLoading(false);
    }
  }, 600);

  return (
    <div>
      <input
        placeholder="Введите город доставки"
        value={city}
        onChange={e => { setCity(e.target.value); calculate(e.target.value); }}
        className="border rounded px-3 py-2 w-full"
      />

      {loading && <p className="text-sm text-gray-400 mt-2">Считаем стоимость...</p>}

      {options.length > 0 && (
        <ul className="mt-3 space-y-2">
          {options.map(opt => (
            <li key={opt.id} className="flex justify-between items-center border rounded px-3 py-2">
              <div>
                <p className="font-medium">{opt.name}</p>
                <p className="text-sm text-gray-500">{opt.daysMin}–{opt.daysMax} дней</p>
              </div>
              <span className="font-semibold">
                {opt.isFree ? 'Бесплатно' : formatPrice(opt.price)}
              </span>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

Подсказка города через DaData

const [suggestions, setSuggestions] = useState<string[]>([]);

const fetchCities = useDebouncedCallback(async (query: string) => {
  const res = await fetch('https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Token ${DADATA_TOKEN}`,
    },
    body: JSON.stringify({ query, from_bound: { value: 'city' }, to_bound: { value: 'city' }, count: 5 }),
  });
  const data = await res.json();
  setSuggestions(data.suggestions.map((s: any) => s.value));
}, 400);

Сроки реализации

  • Схема данных + DeliveryCalculatorService с таблицей зон: 1 день
  • СДЭК API: 1 день
  • Почта России API: 0.5 дня
  • Фронтенд-компонент + подсказка городов: 1 день
  • Кеширование + API-эндпоинт: 0.5 дня

Итого: 3–4 рабочих дня.