Розробка кастомних плагінів Vendure: від ідеї до продакшену

Уявіть: ви налаштували Vendure, але потрібна програма лояльності. Стандартних засобів немає, документація скупа, а готові рішення не гнучкі. Розробка кастомного плагіна — єдиний шлях, але він сповнений підводних каменів: неправильна обробка подій руйнує цілісність даних, помилки в GraphQL-схемі лама

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Розробка кастомних плагінів Vendure: від ідеї до продакшену
Середній
~3-5 днів

Наші компетенції:

Часті запитання

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1422
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1288
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    984
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1250
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    987
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Розробка веб-сайту для компанії ФІКСПЕР
    1001

Уявіть: ви налаштували Vendure, але потрібна програма лояльності. Стандартних засобів немає, документація скупа, а готові рішення не гнучкі. Розробка кастомного плагіна — єдиний шлях, але він сповнений підводних каменів: неправильна обробка подій руйнує цілісність даних, помилки в GraphQL-схемі ламають фронтенд. Ми пройшли через це 40+ разів. Згідно з офіційною документацією Vendure, плагіни — єдиний спосіб розширення без модифікації ядра.

Ми розробляємо кастомні плагіни Vendure під ключ: від аналітики до деплою. Типовий запит клієнта — програма лояльності з балами, але документації Vendure недостатньо, а готові рішення не підходять. Ми беремо на себе повний цикл: аналізуємо вимоги, проектуємо архітектуру, пишемо код, покриваємо тестами та деплоїмо. В результаті ви отримуєте стабільний плагін, який працює під навантаженням і не ламається при оновленні Vendure. За 5 років ми реалізували понад 40 плагінів для Vendure — від простих розширень до складних інтеграцій з ERP.

Проблеми, які ми вирішуємо

  • Розширення GraphQL-схеми: додаємо поля в існуючі типи (наприклад, loyaltyAccount в Customer), не ламаючи зворотну сумісність. Використовуємо extend type та резолвери з ResolveField. Це скорочує час інтеграції на 30%.
  • Асинхронна обробка подій: нарахування балів після завершення замовлення, відправка сповіщень. Підписуємося на OrderPlacedEvent через EventBus.
  • Інтеграція із зовнішніми системами: CRM, ERP, платіжні шлюзи. Плагін може містити HTTP-клієнти та черги повідомлень. Наприклад, при розробці плагіна програми лояльності ми скоротили час на інтеграцію з CRM на 40%.
  • Тестування: використовуємо createTestEnvironment від Vendure для ізольованого тестування без моків. Тести запускаються на in-memory SQLite. Покриття досягає 95%.

Як створити кастомний плагін Vendure для програми лояльності?

Одна з ключових проблем — правильна обробка подій. У Vendure стандартний EventBus працює на NestJS EventEmitter, але потрібно враховувати транзакційність. Ми використовуємо TransactionalConnection для гарантії узгодженості даних. Також важливо не допустити N+1 запитів при розширенні GraphQL-схеми — DataLoader з batch-запитами вирішує цю проблему.

Як тестувати кастомний плагін Vendure?

Для тестування плагіна використовуємо createTestEnvironment від Vendure. Він піднімає повний інстанс Vendure з in-memory SQLite, що дозволяє запускати unit та e2e-тести без зовнішніх залежностей. Тестове покриття досягає 95%, включаючи перевірку обробки подій та GraphQL-запитів.

Структура плагіна Vendure

Кожен плагін — це NestJS модуль з декоратором @VendurePlugin. Рекомендована структура:

src/plugins/loyalty/ ├── loyalty.plugin.ts # Точка входу (NestJS Module) ├── loyalty.service.ts # Бізнес-логіка ├── loyalty.resolver.ts # GraphQL резолвери ├── loyalty.entity.ts # TypeORM сутність ├── loyalty-ui/ # Admin UI розширення (опціонально) │ ├── loyalty.module.ts │ └── components/ └── types.ts # GraphQL типи 

Декоратор @VendurePlugin

// loyalty.plugin.ts import { PluginCommonModule, Type, VendurePlugin } from "@vendure/core"; import { LoyaltyService } from "./loyalty.service"; import { LoyaltyResolver } from "./loyalty.resolver"; import { LoyaltyAccount } from "./loyalty.entity"; import { loyaltyShopApiExtensions, loyaltyAdminApiExtensions } from "./api-extensions"; @VendurePlugin({ imports: [PluginCommonModule], entities: [LoyaltyAccount], shopApiExtensions: { schema: loyaltyShopApiExtensions, resolvers: [LoyaltyResolver], }, adminApiExtensions: { schema: loyaltyAdminApiExtensions, resolvers: [LoyaltyAdminResolver], }, providers: [LoyaltyService], configuration: (config) => { config.orderOptions.orderItemPriceCalculationStrategy = new LoyaltyAwarePriceStrategy(); return config; }, }) export class LoyaltyPlugin {} 

TypeORM сутність

// loyalty.entity.ts import { DeepPartial, Entity, Column, PrimaryGeneratedColumn, ManyToOne, CreateDateColumn, UpdateDateColumn, } from "typeorm"; import { Customer, VendureEntity } from "@vendure/core"; @Entity() export class LoyaltyAccount extends VendureEntity { constructor(input?: DeepPartial<LoyaltyAccount>) { super(input); } @ManyToOne(() => Customer, { onDelete: "CASCADE" }) customer: Customer; @Column() customerId: string; @Column({ default: 0 }) points: number; @Column({ type: "jsonb", nullable: true }) transactions: LoyaltyTransaction[]; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; } interface LoyaltyTransaction { type: "earn" | "spend"; points: number; orderId?: string; reason: string; date: string; } 

Сервіс з EventBus

// loyalty.service.ts import { Injectable } from "@nestjs/common"; import { EventBus, OrderPlacedEvent, RequestContext, TransactionalConnection } from "@vendure/core"; import { OnEvent } from "@nestjs/event-emitter"; import { LoyaltyAccount } from "./loyalty.entity"; @Injectable() export class LoyaltyService implements OnApplicationBootstrap { constructor( private connection: TransactionalConnection, private eventBus: EventBus, ) {} onApplicationBootstrap() { this.eventBus.ofType(OrderPlacedEvent).subscribe(async (event) => { await this.awardPointsForOrder(event.ctx, event.order); }); } async awardPointsForOrder(ctx: RequestContext, order: Order) { const customerId = order.customerId; if (!customerId) return; const pointsToAward = Math.floor(order.totalWithTax / 100); await this.connection.withTransaction(ctx, async (em) => { let account = await em.findOne(LoyaltyAccount, { where: { customerId }, }); if (!account) { account = new LoyaltyAccount({ customerId, points: 0, transactions: [], }); } account.points += pointsToAward; account.transactions = [ ...account.transactions, { type: "earn", points: pointsToAward, orderId: order.id, reason: `Заказ #${order.code}`, date: new Date().toISOString(), }, ]; await em.save(account); }); } async getAccountByCustomer(ctx: RequestContext, customerId: string) { return this.connection .getRepository(ctx, LoyaltyAccount) .findOne({ where: { customerId } }); } async redeemPoints(ctx: RequestContext, customerId: string, points: number) { const account = await this.getAccountByCustomer(ctx, customerId); if (!account || account.points < points) { throw new UserInputError("Недостаточно баллов"); } account.points -= points; account.transactions.push({ type: "spend", points, reason: "Списание при заказе", date: new Date().toISOString(), }); return this.connection.getRepository(ctx, LoyaltyAccount).save(account); } } 

GraphQL Resolver

// loyalty.resolver.ts import { Resolver, Query, Mutation, Args, ResolveField, Parent } from "@nestjs/graphql"; import { Ctx, RequestContext, Allow, Permission, ActiveOrderService } from "@vendure/core"; import { LoyaltyService } from "./loyalty.service"; @Resolver() export class LoyaltyResolver { constructor( private loyaltyService: LoyaltyService, private activeOrderService: ActiveOrderService, ) {} @Query() @Allow(Permission.Owner) async myLoyaltyAccount(@Ctx() ctx: RequestContext) { if (!ctx.activeUserId) return null; return this.loyaltyService.getAccountByCustomer( ctx, ctx.activeUserId.toString() ); } @Mutation() @Allow(Permission.Owner) async redeemLoyaltyPoints( @Ctx() ctx: RequestContext, @Args("points") points: number, ) { const order = await this.activeOrderService.getActiveOrder(ctx, undefined); if (!order) throw new Error("No active order"); await this.loyaltyService.redeemPoints(ctx, ctx.activeUserId!.toString(), points); return order; } } 

Чому кастомний плагін кращий за модифікацію ядра?

Критерій Кастомний плагін Модифікація ядра
Оновлення Vendure Оновлюється незалежно Вимагає злиття змін
Повторне використання Легко переноситься на інші проєкти Прив'язаний до проєкту
Тестування Ізольовані тести Вимагає повного налаштування середовища
Підтримка Документований API Немає гарантій сумісності

Етапи розробки: від концепції до запуску

Етап Тривалість Результат
Аналіз вимог 2-3 дні Технічне завдання, прототип
Проектування архітектури 1-2 дні ER-діаграма, GraphQL-схема
Реалізація ядра 5-7 днів Готовий код з коментарями
Admin UI (опціонально) 2-3 дні Розширення адмінки
Тестування 2-3 дні Unit + e2e, звіт про покриття
Документація та деплой 1 день Інструкція, міграції, викладка

Що входить в роботу

  • Вихідний код плагіна з коментарями українською
  • Повна документація: встановлення, налаштування, інтеграція
  • Інструкція з міграції бази даних
  • Тестове покриття (unit + e2e) з використанням createTestEnvironment
  • Підтримка протягом 2 тижнів після здачі (фікс багів, консультації)
Типові помилки при розробці
  • Не підписуються на події в onApplicationBootstrap — EventBus не спрацьовує.
  • Використовують прямий запит до TypeORM замість TransactionalConnection — втрачається цілісність даних.
  • Не вказують сутності в entities — таблиці не створюються.
  • Плутають Shop API і Admin API при розширенні схеми — резолвери не працюють.

Терміни та вартість

Терміни розробки кастомного плагіна: від 2 тижнів (прості розширення) до 6 тижнів (складні інтеграції). Вартість розраховується індивідуально після аналізу вимог. Ми пропонуємо фіксовану ціну та прозорі етапи оплати.

Довіряйте досвіду

Ми — команда сертифікованих розробників Vendure з 5-річним досвідом. За плечима більше 40 успішно запущених плагінів. Гарантуємо стабільність та своєчасну підтримку. Офіційна документація Vendure підтверджує, що плагіни — єдиний правильний спосіб розширення. Фреймворк NestJS забезпечує модульність і тестованість.

Зв'яжіться з нами, щоб обговорити ваш проєкт. Отримайте консультацію з розробки плагіна Vendure.