manicanldes-backend/src/coupons/coupon-policy.ts

60 lines
1.9 KiB
TypeScript

import type { Coupon, Prisma } from '../generated/prisma/client';
import type { Principal } from '../identity/identity.types';
import { AppError } from '../common/errors/app-error';
import { minor, discountFor } from '../checkout/money';
export function assertCoupon(
coupon: Coupon | null,
currency: string,
subtotal: bigint,
now: Date,
uses: number,
userUses: number,
): asserts coupon is Coupon {
if (!coupon) throw new AppError('COUPON_INELIGIBLE', 'COUPON_UNKNOWN');
const failures: [boolean, string][] = [
[!coupon.active, 'COUPON_DISABLED'],
[coupon.currency !== currency, 'COUPON_CURRENCY'],
[coupon.startsAt > now, 'COUPON_NOT_STARTED'],
[coupon.endsAt <= now, 'COUPON_EXPIRED'],
[minor(coupon.minimumSubtotal.toString()) > subtotal, 'COUPON_MINIMUM'],
[uses >= coupon.maxUses, 'COUPON_TOTAL_LIMIT'],
[userUses >= coupon.perUserLimit, 'COUPON_USER_LIMIT'],
];
const failure = failures.find(([failed]) => failed);
if (failure) throw new AppError('COUPON_INELIGIBLE', failure[1]);
}
export async function priceCoupon(
tx: Prisma.TransactionClient,
actor: Principal,
code: string | undefined,
currency: string,
subtotal: bigint,
now: Date,
) {
if (!code) return { discount: 0n, coupon: null };
const coupon = await tx.coupon.findUnique({
where: {
organizationId_code: { organizationId: actor.organizationId, code },
},
});
const where = {
couponId: coupon?.id ?? '00000000-0000-0000-0000-000000000000',
status: 'PENDING_PAYMENT' as const,
expiresAt: { gt: now },
};
const uses = await tx.order.count({ where });
const userUses = await tx.order.count({
where: { ...where, userId: actor.userId },
});
assertCoupon(coupon, currency, subtotal, now, uses, userUses);
return {
coupon,
discount: discountFor(subtotal, {
kind: coupon.kind,
amount: coupon.amount?.toString() ?? null,
percentBps: coupon.percentBps,
}),
};
}