Apply Coupon
Authenticated. Validates a coupon code against expiry, applicability, min purchase, total / per-user usage caps under a SELECT FOR UPDATE row lock. Returns the calculated discount.
POST /api/v1/referral/coupon/apply โ ๐ Bearer ยท Rate limit: 10 req / minute ยท Kill-switched
Validates a coupon code against the active rules + the user's prior usage, calculates the discount with Decimal arithmetic (no float rounding), and records a CouponUsage row โ all under a SELECT ... FOR UPDATE row lock so concurrent applications can't both pass the maxUses check.
TOCTOU-safe. The full validation chain (active flag, time window, applicability, min purchase, total uses, per-user uses) plus the CouponUsage insert run inside one transaction with the coupon row locked via SELECT ... FOR UPDATE. Two concurrent requests for the last available slot can't both succeed.
Discount is calculated, not stored as money. The endpoint returns the discount value (a number) โ the actual money side-effect happens in the calling flow (e.g. wallet load, message send) which deducts this amount from the transaction price. The server records the usage; the caller is responsible for actually applying the discount to their domain transaction.
Request
Body โ ApplyCouponDto
| Field | Type | Required | Validation | Notes |
|---|---|---|---|---|
code | string | โ | IsString() | The coupon code to apply (case-sensitive โ server does NOT uppercase before lookup) |
transactionAmount | number | โ | IsNumber(), Min(1) | The amount the coupon is being applied against, in dollars (NOT cents). Used for minPurchase check + percentage calculation. |
appliesTo | string | โ | IsString() | What domain the coupon is being applied to (e.g. message). Server matches against Coupon.appliesTo ('BOTH' accepts any). |
| Header | Required | Notes |
|---|---|---|
Authorization: Bearer <accessToken> | โ | JWT from POST /auth/login |
Response
200 OK โ ApiResponseOf<ApplyCouponResponseDto>
{
"success": true,
"data": {
"discount": 2.50
}
}| Field | Type | Notes |
|---|---|---|
discount | number | Calculated discount in dollars (already capped at transactionAmount โ won't exceed the input). 2 decimal places. |
Errors
| HTTP | code / i18nKey | Payload | Reason |
|---|---|---|---|
400 | referral.coupon.invalid | โ | Code unknown OR Coupon.active = false |
400 | referral.coupon.not_active | โ | Coupon.startsAt is in the future |
400 | referral.coupon.expired | โ | Coupon.endsAt is in the past |
400 | referral.coupon.not_applicable | โ | Coupon.appliesTo !== 'BOTH' AND !== submitted appliesTo |
400 | referral.coupon.min_purchase | { min } | transactionAmount < Coupon.minPurchase |
400 | referral.coupon.usage_limit | โ | Total CouponUsage count >= Coupon.maxUses |
400 | referral.coupon.already_used | โ | This user's CouponUsage count >= Coupon.perUserLimit (default 1) |
400 | (DTO validation) | โ | Missing fields / transactionAmount < 1 |
401 | (guard) | โ | Missing / invalid bearer token |
429 | (throttle) | โ | Rate limit exceeded (10 req/min) |
503 | features.referral_disabled | โ | Admin kill switch REFERRAL is active |
Side effects
Inside one transaction (timeout: 10s):
- Lock the coupon row โ
SELECT ... FOR UPDATEonCouponbycode. Missing ORactive = falseโ throwinvalid. - Time window โ reject if
startsAt > now(not_active) ORendsAt < now(expired). - Applicability โ reject if
coupon.appliesTo !== 'BOTH' && coupon.appliesTo !== submitted appliesTo. - Minimum purchase โ reject if
coupon.minPurchaseset ANDtransactionAmount < minPurchase. Error carries{ min }payload. - Total usage cap โ
couponUsage.count({ where: { couponId } }); if>= maxUses, throwusage_limit. - Per-user cap โ
couponUsage.count({ where: { couponId, userId } }); if>= perUserLimit(default 1), throwalready_used. - Discount calculation (Prisma.Decimal arithmetic):
FIXEDtype โdiscount = coupon.value.PERCENTAGEtype โdiscount = transactionAmount * (coupon.value / 100).- Cap at transaction amount โ
discount = min(discount, transactionAmount). - Round to 2 decimal places.
- Record usage โ
couponUsage.create({ id, couponId, userId, amount: discount }).
After the transaction commits, return { discount: discountDec.toNumber() }. Audit log: [coupon] Applied <code>: $<amount> off for user <userId>.
Code samples
curl -X POST https://api.bio.re/api/v1/referral/coupon/apply \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"code": "WELCOME20",
"transactionAmount": 10,
"appliesTo": "message"
}'type ApplyCouponInput = {
code: string;
transactionAmount: number;
appliesTo: string;
};
type ApplyCouponResult = {
discount: number;
};
async function applyCoupon(accessToken: string, input: ApplyCouponInput): Promise<ApplyCouponResult> {
const res = await fetch('https://api.bio.re/api/v1/referral/coupon/apply', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Coupon apply failed'), {
code: json?.error?.code,
min: json?.error?.min, // present on 'min_purchase'
});
}
return json.data;
}import { useMutation } from '@tanstack/react-query';
export function useApplyCoupon() {
return useMutation({
mutationFn: async (input: ApplyCouponInput) => {
const res = await fetch('/api/v1/referral/coupon/apply', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
});
const json = await res.json();
if (!res.ok || !json.success) {
throw Object.assign(new Error(json?.error?.message ?? 'Coupon apply failed'), {
code: json?.error?.code,
i18nKey: json?.error?.i18nKey,
min: json?.error?.min,
});
}
return json.data as ApplyCouponResult;
},
});
}Try it
Authorization
bearer In: header
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
curl -X POST "https://loading/api/v1/referral/coupon/apply" \ -H "Content-Type: application/json" \ -d '{ "code": "WELCOME20", "transactionAmount": 10, "appliesTo": "message" }'{
"success": true,
"data": {
"discount": 10
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}{
"success": false,
"error": {
"code": "AUTH_UNAUTHORIZED",
"message": "Invalid credentials",
"i18nKey": "auth.login.invalid_credentials",
"i18nVars": {
"field": "email"
},
"details": [
{
"message": "email must be an email"
}
],
"correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}Source
| Source | Path | Lines |
|---|---|---|
| Controller | apps/api-core/src/modules/referral/referral.controller.ts | 66โ77 (applyCoupon) |
| DTO (request) | apps/api-core/src/modules/referral/dto/index.ts | 5โ14 (ApplyCouponDto) |
| DTO (response) | apps/api-core/src/modules/referral/dto/referral-response.dto.ts | 382โ385 (ApplyCouponResponseDto) |
| Service | apps/api-core/src/modules/referral/referral.service.ts | 392โ442 (applyCoupon โ TOCTOU-safe transaction with row lock) |
| Prisma models | packages/prisma/prisma/schema.prisma | Coupon (code unique, type enum FIXED/PERCENTAGE, appliesTo, maxUses, perUserLimit, minPurchase, time window), CouponUsage (records each use), enum CouponType |
Get Referral Dashboard
Authenticated. Returns the user's referral link snapshot (clicks, signups, conversions, totalEarnings) plus their last 50 active rewards (clawed-back rewards excluded).
List My Tickets
Owner-scoped paginated list of the current user's support tickets. Filter by status. Always returns YOUR tickets โ no admin lens, no cross-user reads possible.