Problem
There is no promo code or discount system on ManageHub. Admins cannot create promotional codes for onboarding campaigns, corporate rates, or seasonal discounts. Members have no way to apply a discount at checkout, which limits the platform's marketing flexibility.
Proposed Solution
Create a new NestJS module at backend/src/promo-codes/.
Entity: PromoCode
id (UUID), code (string, unique — stored uppercase)
discountType (enum: percentage | fixed_amount)
discountValue (integer — percentage 1–100, or fixed kobo amount)
maxUses (integer, nullable — null = unlimited)
usedCount (integer, default 0)
minBookingAmount (integer, kobo — minimum booking value to qualify, default 0)
applicableWorkspaceTypes (array of WorkspaceType enum, nullable — null = all types)
expiresAt (timestamp, nullable)
isActive (boolean, default true)
createdAt, updatedAt
Entity: PromoCodeUsage
id (UUID), promoCodeId (FK), userId (FK), bookingId (FK)
discountApplied (integer, kobo), usedAt (timestamp)
Endpoints:
| Method |
Path |
Access |
POST |
/promo-codes |
Admin |
GET |
/promo-codes |
Admin |
PATCH |
/promo-codes/:id |
Admin |
DELETE |
/promo-codes/:id |
Admin |
POST |
/promo-codes/validate |
Authenticated member |
POST /promo-codes/validate accepts { code, workspaceId, bookingAmount } and returns { valid: boolean, discountType, discountValue, finalAmount, message }. The payment service then reads this when initialising a booking payment.
Acceptance Criteria
Problem
There is no promo code or discount system on ManageHub. Admins cannot create promotional codes for onboarding campaigns, corporate rates, or seasonal discounts. Members have no way to apply a discount at checkout, which limits the platform's marketing flexibility.
Proposed Solution
Create a new NestJS module at
backend/src/promo-codes/.Entity:
PromoCodeid(UUID),code(string, unique — stored uppercase)discountType(enum:percentage|fixed_amount)discountValue(integer — percentage 1–100, or fixed kobo amount)maxUses(integer, nullable —null= unlimited)usedCount(integer, default0)minBookingAmount(integer, kobo — minimum booking value to qualify, default0)applicableWorkspaceTypes(array ofWorkspaceTypeenum, nullable —null= all types)expiresAt(timestamp, nullable)isActive(boolean, defaulttrue)createdAt,updatedAtEntity:
PromoCodeUsageid(UUID),promoCodeId(FK),userId(FK),bookingId(FK)discountApplied(integer, kobo),usedAt(timestamp)Endpoints:
POST/promo-codesGET/promo-codesPATCH/promo-codes/:idDELETE/promo-codes/:idPOST/promo-codes/validatePOST /promo-codes/validateaccepts{ code, workspaceId, bookingAmount }and returns{ valid: boolean, discountType, discountValue, finalAmount, message }. The payment service then reads this when initialising a booking payment.Acceptance Criteria
codeis normalised to uppercase on savePOST /promo-codes/validatechecks: code exists,isActive, not expired, undermaxUses, workspace type matches, booking amount meetsminBookingAmountPromoCodeUsageunique constraint onpromoCodeId + userId)usedCountincrements atomically when a payment using the code succeeds (hook intohandle-webhook.provider.ts)PromoCodesModuleis registered inAppModule