Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/lib/api/sales/calculate-sale-earnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const calculateSaleEarnings = ({
if (reward.type === "flat") {
return sale.quantity * amount;
} else if (reward.type === "percentage") {
return sale.amount * (amount / 100);
return Math.round(sale.amount * (amount / 100));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

return 0;
Expand Down
121 changes: 118 additions & 3 deletions apps/web/tests/commissions/create-commission.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { CommissionResponse } from "@/lib/types";
import { describe, expect, test } from "vitest";
import { randomCustomer, randomId } from "../utils/helpers";
import { createId } from "@/lib/api/create-id";
import { generateRandomName } from "@/lib/names";
import { conn } from "@/lib/planetscale";
import { prisma } from "@/lib/prisma";
import { CommissionResponse, EnrolledPartnerProps } from "@/lib/types";
import { Prisma } from "@prisma/client";
import { describe, expect, onTestFinished, test } from "vitest";
import { randomCustomer, randomId, randomPartnerEmail } from "../utils/helpers";
import { IntegrationHarness } from "../utils/integration";
import {
E2E_CUSTOMER_ID,
E2E_LEAD_REWARD,
E2E_PARTNER,
E2E_PARTNER_GROUP,
E2E_PROGRAM,
} from "../utils/resource";
import { verifyCommission } from "../utils/verify-commission";

Expand Down Expand Up @@ -149,6 +156,114 @@ describe.concurrent("POST /commissions", async () => {
});
});

test("create sale commission rounds 3¢ at 20% to 1¢", async () => {
let partnerId: string | undefined;
let rewardId: string | undefined;

onTestFinished(async () => {
if (partnerId) {
const commissions = await prisma.commission.findMany({
where: { partnerId, programId: E2E_PROGRAM.id },
select: { id: true },
});

if (commissions.length > 0) {
await prisma.activityLog.deleteMany({
where: {
resourceType: "commission",
resourceId: { in: commissions.map((c) => c.id) },
},
});
}

await prisma.commission.deleteMany({
where: { partnerId, programId: E2E_PROGRAM.id },
});
await prisma.customer.deleteMany({
where: { partnerId },
});
await prisma.link.deleteMany({
where: { partnerId },
});
await prisma.programEnrollment.deleteMany({
where: { partnerId, programId: E2E_PROGRAM.id },
});
await conn.execute(`DELETE FROM Partner WHERE id = ?`, [partnerId]);
}

if (rewardId) {
await prisma.reward.deleteMany({
where: { id: rewardId },
});
}
});

const { status: partnerStatus, data: partner } =
await http.post<EnrolledPartnerProps>({
path: "/partners",
body: {
name: generateRandomName(),
email: randomPartnerEmail(),
groupId: E2E_PARTNER_GROUP.id,
},
});

expect(partnerStatus).toEqual(201);
partnerId = partner.id;

const reward = await prisma.reward.create({
data: {
id: createId({ prefix: "rw_" }),
programId: E2E_PROGRAM.id,
event: "sale",
type: "percentage",
amountInPercentage: new Prisma.Decimal(20),
},
});
rewardId = reward.id;

await prisma.programEnrollment.update({
where: {
partnerId_programId: {
partnerId,
programId: E2E_PROGRAM.id,
},
},
data: {
saleRewardId: reward.id,
},
});

const invoiceId = `INV_${randomId()}`;
const customer = randomCustomer();

const { status, data } = await http.post<any>({
path: "/commissions",
body: {
type: "sale",
partnerId,
saleAmount: 3,
invoiceId,
customer: {
externalId: customer.externalId,
email: customer.email,
name: customer.name,
country: "US",
},
},
});

expect(status).toEqual(202);
expect(data).toStrictEqual(expectedQueuedResponse);

await verifyCommission({
http,
invoiceId,
expectedSaleAmount: 3,
expectedEarnings: 1,
});
});

test("error when customer is not found", async () => {
const { status, data } = await http.post<any>({
path: "/commissions",
Expand Down
143 changes: 143 additions & 0 deletions apps/web/tests/sales/calculate-sale-earnings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { calculateSaleEarnings } from "@/lib/api/sales/calculate-sale-earnings";
import { describe, expect, test } from "vitest";

const percentageEarnings = (
saleAmount: number,
percent: number,
quantity = 1,
) =>
calculateSaleEarnings({
reward: {
type: "percentage",
amountInCents: null,
amountInPercentage: percent,
},
sale: { amount: saleAmount, quantity },
});

describe("calculateSaleEarnings", () => {
describe("percentage – same as the old truncate path", () => {
test.each([
{
label: "whole cents: $10 sale at 20%",
saleAmount: 1000,
percent: 20,
expected: 200,
},
{
label: "whole cents: $10 sale at 50%",
saleAmount: 1000,
percent: 50,
expected: 500,
},
{
label: "whole cents: $19 sale at 10%",
saleAmount: 1900,
percent: 10,
expected: 190,
},
{
label: "1.4¢ truncates and rounds to the same value",
saleAmount: 7,
percent: 20,
expected: 1,
},
{
label: "0.4¢ stays 0 (below half a cent)",
saleAmount: 2,
percent: 20,
expected: 0,
},
{
label: "zero sale amount",
saleAmount: 0,
percent: 20,
expected: 0,
},
{
label: "zero percent",
saleAmount: 1000,
percent: 0,
expected: 0,
},
])("$label → $expected", ({ saleAmount, percent, expected }) => {
expect(percentageEarnings(saleAmount, percent)).toBe(expected);
});
});

describe("percentage – half-up (new vs old truncate)", () => {
test.each([
{
label: "3¢ sale at 20% (ticket: 0.6¢, used to store 0)",
saleAmount: 3,
percent: 20,
expected: 1,
},
{
label: "15¢ sale at 10% (1.5¢, used to store 1)",
saleAmount: 15,
percent: 10,
expected: 2,
},
{
label: "1¢ sale at 50% (0.5¢, used to store 0)",
saleAmount: 1,
percent: 50,
expected: 1,
},
])("$label → $expected", ({ saleAmount, percent, expected }) => {
expect(percentageEarnings(saleAmount, percent)).toBe(expected);
});
});

test("percentage ignores quantity (uses sale amount only)", () => {
expect(percentageEarnings(1000, 20, 5)).toBe(200);
});

describe("flat", () => {
test.each([
{
label: "single sale",
amountInCents: 5000,
quantity: 1,
expected: 5000,
},
{
label: "quantity multiplies the flat amount",
amountInCents: 500,
quantity: 2,
expected: 1000,
},
{
label: "zero quantity",
amountInCents: 500,
quantity: 0,
expected: 0,
},
])("$label → $expected", ({ amountInCents, quantity, expected }) => {
expect(
calculateSaleEarnings({
reward: {
type: "flat",
amountInCents,
amountInPercentage: null,
},
sale: { amount: 1000, quantity },
}),
).toBe(expected);
});
});

test("returns 0 when reward type is neither flat nor percentage", () => {
expect(
calculateSaleEarnings({
reward: {
type: "unknown" as "flat",
amountInCents: 500,
amountInPercentage: 20,
},
sale: { amount: 1000, quantity: 1 },
}),
).toBe(0);
});
});
Loading