60 lines
1.9 KiB
TypeScript
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,
|
|
}),
|
|
};
|
|
}
|