multivendor-coupon
v0.0.1
Published
Install-and-use coupon plugin with TypeORM store, validation hooks, redemption ledger, and HTTP routes for multi-vendor marketplaces
Maintainers
Readme
multivendor-coupon
Install-and-use coupon/promotion plugin for multi-vendor marketplaces: admin CRUD, scope/audience targeting, checkout validation, and a redemption ledger with usage limits.
Features
- Admin CRUD for coupons (percent / fixed-amount / free-shipping)
- Scope targeting: ALL / CATEGORY / PRODUCT
- Audience targeting: ALL / USERS / CUSTOMER GROUPS
- Checkout validation (
/coupon/validate) with min/max purchase, email restriction, vendor scope, usage limits, and platform-sale interaction rules coupon_redemptionledger for global + per-customer usage limits, with release on RTO/cancellation- TypeORM entities + migrations (guarded with
hasTable/hasColumn, safe to re-run) - routing-controllers HTTP (or express router)
Install
pnpm add multivendor-couponHost integration
The package never touches host tables (product, order, category,
customer_group, vendor ownership, etc.) directly — every lookup that needs host data
is injected through CouponHostPorts.
import { registerCouponPlugin, getCouponOrmMetadata, COUPON_PLUGIN_TOKEN } from 'multivendor-coupon';
import { Container } from 'typedi';
import { getConnection } from 'typeorm';
const couponPlugin = registerCouponPlugin({
dataSource: getConnection(),
flags: { enabled: true },
http: { mode: 'routing-controllers' },
ports: {
expandCategoryIds: async (ids) => hostCategoryService.expandWithDescendants(ids),
filterActiveProductIds: async (ids) => hostProductService.filterActive(ids),
getProductCategoryMap: async (ids) => hostProductService.categoryMapFor(ids),
getVendorProductIds: async (vendorId, ids) => hostVendorProductService.filterOwned(vendorId, ids),
isPlatformCustomerGroupLive: async (groupId) => hostCustomerGroupService.isLivePlatformGroup(groupId),
countPriorCustomerOrders: async (customerId, excludedStatuses) =>
hostOrderService.countPriorOrders(customerId, excludedStatuses),
countLegacyCouponUsage: async (couponCode, customerId) =>
hostOrderService.countLegacyCouponUsage(couponCode, customerId),
validatePlatformCustomerGroups: async (groupIds) =>
hostCustomerGroupService.validatePlatformGroups(groupIds),
resolveCartAndPrice: async ({ cartIds, orderItems, customerId, customerGroupId }) => {
// Resolve host cart -> priced order items + subtotal, or price the given orderItems.
return hostOrderCreationService.priceForCoupon({ cartIds, orderItems, customerId, customerGroupId });
},
},
});
Container.set(COUPON_PLUGIN_TOKEN, couponPlugin);
// ORM
const couponOrm = getCouponOrmMetadata();
// entities: [...hostEntities, ...couponOrm.entities]
// migrations: [...hostMigrations, ...couponOrm.migrations]
// Controllers
// controllers: [...host, ...(couponPlugin.http.controllers ?? [])]Order-placement hook (usage limits)
Call inside the same DB transaction that creates the order, after locking the coupon row:
import { resolveCouponPlugin } from 'multivendor-coupon';
await connection.manager.transaction(async (em) => {
const coupon = await em.getRepository(CouponEntity).findOne({
where: { couponId },
lock: { mode: 'pessimistic_write' },
});
await resolveCouponPlugin().client.assertUsageWithinLimitsInTransaction(coupon, customerId, em);
await resolveCouponPlugin().client.createRedemption(
{ couponId, orderId, customerId, couponCode: coupon.couponCode, discountAmount },
em
);
});Release on cancellation / RTO
import { releaseCouponForOrder } from 'multivendor-coupon/hooks';
const { patch, released } = await releaseCouponForOrder(orderId, order, 'rto');
if (released.released) {
await orderRepository.update(orderId, patch);
}Publish
pnpm run build && pnpm run test
# tag: coupon-v0.0.1