apps/recallassess/recallassess-api/src/api/client/promo-code/promo-code.service.ts
CLPromoCodeService
Client portal API — Promo Code (Promo Code). Business Logic Service for RecallAssess.
Methods |
|
constructor(prisma: BNestPrismaService)
|
||||||
|
Parameters :
|
| calculateDiscount | |||||||||
calculateDiscount(originalAmount: number, validation: PromoCodeValidationDto)
|
|||||||||
|
Calculate discount amount based on promo code validation and original amount
Parameters :
Returns :
number
|
| Private Async findByCode | ||||||
findByCode(code: string)
|
||||||
|
Promo codes are stored uppercase; lookup is case-insensitive for legacy rows.
Parameters :
Returns :
unknown
|
| Async getPromoCodeDetails | ||||||
getPromoCodeDetails(code: string)
|
||||||
|
Get detailed info about a valid promo code (Only returns data if code is valid)
Parameters :
Returns :
Promise<ValidPromoCodeDto | null>
|
| Async incrementUsage | ||||||
incrementUsage(code: string)
|
||||||
|
Increment usage count when a promo code is applied This should be called after a successful purchase/subscription
Parameters :
Returns :
Promise<void>
|
| Async validatePromoCode | ||||||
validatePromoCode(code: string)
|
||||||
|
Validate a promo code Checks if code exists, is active, not expired, and not sold out
Parameters :
Returns :
Promise<PromoCodeValidationDto>
|
import { bnestPlainToDto } from "@bish-nest/core";
import { BNestPrismaService } from "@bish-nest/core/services/database/prisma/prisma.service";
import { Injectable } from "@nestjs/common";
import { PromoCodeDiscountType } from "@prisma/client";
import { calculatePromoDiscountAmount } from "@recallassess/shared-ng";
import { PromoCodeValidationDto, ValidPromoCodeDto } from "./dto";
/**
* CLPromoCodeService
*
* Client portal API — Promo Code (Promo Code). Business Logic Service for RecallAssess.
*/
@Injectable()
export class CLPromoCodeService {
constructor(private readonly prisma: BNestPrismaService) {}
/** Promo codes are stored uppercase; lookup is case-insensitive for legacy rows. */
private async findByCode(code: string) {
const normalized = code.trim();
if (!normalized) {
return null;
}
return this.prisma.client.promoCode.findFirst({
where: {
promo_code: {
equals: normalized,
mode: "insensitive",
},
},
});
}
private toDiscountPayload(promoCode: {
discount_type: PromoCodeDiscountType;
discount_percentage: { toNumber?: () => number } | number | null;
discount_amount: { toNumber?: () => number } | number | null;
}) {
const discountType = promoCode.discount_type ?? PromoCodeDiscountType.PERCENTAGE;
const discountPercentage =
promoCode.discount_percentage != null ? Number(promoCode.discount_percentage) : undefined;
const discountAmount =
promoCode.discount_amount != null ? Number(promoCode.discount_amount) : undefined;
return {
discount_type: discountType,
discount_percentage:
discountType === PromoCodeDiscountType.PERCENTAGE ||
discountType === PromoCodeDiscountType.PERCENTAGE_UP_TO
? discountPercentage
: undefined,
discount_amount:
discountType === PromoCodeDiscountType.FIXED_AMOUNT ||
discountType === PromoCodeDiscountType.PERCENTAGE_UP_TO
? discountAmount
: undefined,
};
}
/**
* Validate a promo code
* Checks if code exists, is active, not expired, and not sold out
*/
async validatePromoCode(code: string): Promise<PromoCodeValidationDto> {
const promoCode = await this.findByCode(code);
// Code doesn't exist
if (!promoCode) {
return bnestPlainToDto(
{
is_valid: false,
message: "Promo code not found",
error_code: "NOT_FOUND",
},
PromoCodeValidationDto,
);
}
// Code is inactive
if (!promoCode.is_active) {
return bnestPlainToDto(
{
is_valid: false,
promo_code: promoCode.promo_code,
message: "This promo code is no longer active",
error_code: "INACTIVE",
},
PromoCodeValidationDto,
);
}
const now = new Date();
// Code hasn't started yet
if (promoCode.valid_from && new Date(promoCode.valid_from) > now) {
return bnestPlainToDto(
{
is_valid: false,
promo_code: promoCode.promo_code,
message: "This promo code is not yet valid",
error_code: "NOT_STARTED",
},
PromoCodeValidationDto,
);
}
// Code has expired
if (promoCode.valid_until && new Date(promoCode.valid_until) < now) {
return bnestPlainToDto(
{
is_valid: false,
promo_code: promoCode.promo_code,
message: "This promo code has expired",
error_code: "EXPIRED",
},
PromoCodeValidationDto,
);
}
// Code usage limit reached (sold out)
if (promoCode.usage_limit && promoCode.usage_count >= promoCode.usage_limit) {
return bnestPlainToDto(
{
is_valid: false,
promo_code: promoCode.promo_code,
message: "This promo code has reached its usage limit",
error_code: "SOLD_OUT",
},
PromoCodeValidationDto,
);
}
// Code is valid!
return bnestPlainToDto(
{
is_valid: true,
promo_code: promoCode.promo_code,
...this.toDiscountPayload(promoCode),
message: "Promo code is valid",
},
PromoCodeValidationDto,
);
}
/**
* Get detailed info about a valid promo code
* (Only returns data if code is valid)
*/
async getPromoCodeDetails(code: string): Promise<ValidPromoCodeDto | null> {
const validation = await this.validatePromoCode(code);
if (!validation.is_valid) {
return null;
}
const promoCode = await this.findByCode(code);
if (!promoCode) {
return null;
}
const remaining_uses = promoCode.usage_limit
? Math.max(0, promoCode.usage_limit - promoCode.usage_count)
: null;
return bnestPlainToDto(
{
promo_code: promoCode.promo_code,
title: promoCode.title,
...this.toDiscountPayload(promoCode),
valid_until: promoCode.valid_until,
remaining_uses,
},
ValidPromoCodeDto,
);
}
/**
* Increment usage count when a promo code is applied
* This should be called after a successful purchase/subscription
*/
async incrementUsage(code: string): Promise<void> {
const promoCode = await this.findByCode(code);
if (!promoCode) {
return;
}
await this.prisma.client.promoCode.update({
where: { id: promoCode.id },
data: {
usage_count: {
increment: 1,
},
},
});
}
/**
* Calculate discount amount based on promo code validation and original amount
*/
calculateDiscount(originalAmount: number, validation: PromoCodeValidationDto): number {
return calculatePromoDiscountAmount(originalAmount, {
discount_type: validation.discount_type,
discount_percentage: validation.discount_percentage,
discount_amount: validation.discount_amount,
});
}
}