learning_ai_common_plat/services/growth-service/src/modules/referrals/routes.ts
saravanakumardb1 b94510aeb9 feat(services): add growth-service (invitations, referrals, promos)
- Copied as-is from learning_voice_ai_agent/services/growth-service
- 33 tests passing (vitest)
- Fastify 5 + Cosmos DB + Stripe + Zod
- Modules: invitations, referrals, promos
- Port 4001
2026-02-12 11:39:11 -08:00

125 lines
4.2 KiB
TypeScript

/**
* Referral REST endpoints.
*
* GET /referrals — list all referrals
* GET /referrals/stats — count stats (total, completed, rewarded)
* GET /referrals/by-referrer/:referrerId — list by referrer
* GET /referrals/by-email/:email — get by referred email
* POST /referrals — create a referral
* PUT /referrals/:id — update status
*/
import type { FastifyInstance } from "fastify";
import { PRODUCT_ID } from "../../lib/product-config.js";
import { BadRequestError, NotFoundError } from "../../lib/errors.js";
import { dispatchReferralStatusChanged } from "../../lib/webhooks.js";
import * as repo from "./repository.js";
import {
CreateReferralSchema,
UpdateReferralStatusSchema,
type ReferralDoc,
} from "./types.js";
export async function referralRoutes(app: FastifyInstance) {
// List all
app.get("/referrals", async (req) => {
const { limit = "100", offset = "0" } = req.query as Record<string, string>;
const items = await repo.listAll(Number(limit), Number(offset));
return { referrals: items, count: items.length };
});
// Stats
app.get("/referrals/stats", async () => {
return repo.countReferrals();
});
// By referrer
app.get("/referrals/by-referrer/:referrerId", async (req) => {
const { referrerId } = req.params as { referrerId: string };
const items = await repo.getByReferrer(referrerId);
return { referrals: items, count: items.length };
});
// By referred email
app.get("/referrals/by-email/:email", async (req) => {
const { email } = req.params as { email: string };
const doc = await repo.getByReferredEmail(email);
if (!doc) throw new NotFoundError("Referral not found");
return doc;
});
// Create
app.post("/referrals", async (req, reply) => {
const parsed = CreateReferralSchema.safeParse(req.body);
if (!parsed.success) {
throw new BadRequestError(parsed.error.issues.map((i) => i.message).join("; "));
}
const input = parsed.data;
// Check if referral already exists for this email
const existing = await repo.getByReferredEmail(input.referredEmail);
if (existing) {
throw new BadRequestError("A referral already exists for this email");
}
const now = new Date().toISOString();
const doc: ReferralDoc = {
id: `ref_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
productId: PRODUCT_ID,
referrerId: input.referrerId,
referrerEmail: input.referrerEmail,
referredUserId: null,
referredEmail: input.referredEmail.toLowerCase(),
status: "pending",
referrerRewardTokens: input.referrerRewardTokens,
referredRewardTokens: input.referredRewardTokens,
referrerRewarded: false,
referredRewarded: false,
createdAt: now,
completedAt: null,
};
const created = await repo.create(doc);
reply.code(201);
return created;
});
// Update status
app.put("/referrals/:id", async (req) => {
const { id } = req.params as { id: string };
const { referrerId } = req.query as { referrerId?: string };
if (!referrerId) {
throw new BadRequestError("referrerId query parameter is required");
}
const parsed = UpdateReferralStatusSchema.safeParse(req.body);
if (!parsed.success) {
throw new BadRequestError(parsed.error.issues.map((i) => i.message).join("; "));
}
const updates: Partial<ReferralDoc> = { ...parsed.data };
if (
parsed.data.status === "rewarded" ||
parsed.data.status === "subscribed" ||
parsed.data.status === "signed_up"
) {
updates.completedAt = updates.completedAt ?? new Date().toISOString();
}
const updated = await repo.update(id, referrerId, updates);
if (!updated) throw new NotFoundError("Referral not found");
// Fire-and-forget webhook callback on status transition
dispatchReferralStatusChanged({
referralId: updated.id,
referrerId: updated.referrerId,
referredEmail: updated.referredEmail,
previousStatus: parsed.data.status, // requested status
newStatus: updated.status,
referrerRewarded: updated.referrerRewarded,
referredRewarded: updated.referredRewarded,
});
return updated;
});
}