Files
cfw-autumn/server/src/internal/api/customers/cusRouter.ts
2025-03-31 21:03:42 +01:00

393 lines
11 KiB
TypeScript

import {
AppEnv,
CreateCustomer,
CreateCustomerSchema,
CusProductStatus,
Customer,
CustomerResponseSchema,
} from "@autumn/shared";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { ErrCode } from "@autumn/shared";
import { ErrorMessages } from "@/errors/errMessages.js";
import { Router } from "express";
import { StatusCodes } from "http-status-codes";
import { CusService } from "../../customers/CusService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { EventService } from "../events/EventService.js";
import { getCustomerDetails } from "./cusUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { getCusBalancesByEntitlement } from "@/internal/customers/entitlements/cusEntUtils.js";
import {
fullCusProductToCusEnts,
fullCusProductToCusPrices,
} from "@/internal/customers/products/cusProductUtils.js";
import { deleteCusById } from "./handlers/cusDeleteHandlers.js";
import { handleUpdateBalances } from "./handlers/handleUpdateBalances.js";
import { handleUpdateEntitlement } from "./handlers/handleUpdateEntitlement.js";
import { handleCusProductExpired } from "./handlers/handleCusProductExpired.js";
import { handleAddCouponToCus } from "./handlers/handleAddCouponToCus.js";
import { handlePostCustomerRequest } from "./handlers/handleCreateCustomer.js";
import { notNullish } from "@/utils/genUtils.js";
import { entityRouter } from "../entities/entityRouter.js";
export const cusRouter = Router();
cusRouter.post("/all/search", async (req: any, res: any) => {
try {
const { search, page_size = 50, page = 1, last_item, filters } = req.body;
const { data: customers, count } = await CusService.searchCustomers({
sb: req.sb,
orgId: req.orgId,
env: req.env,
search,
filters,
lastItem: last_item,
pg: req.pg,
pageNumber: page,
pageSize: page_size,
});
res.status(200).json({ customers, totalCount: count });
} catch (error) {
handleRequestError({ req, error, res, action: "search customers" });
}
});
cusRouter.get("", async (req: any, res: any) => {
try {
const customers = await CusService.getCustomers(req.sb, req.orgId, req.env);
res.status(200).send({ customers });
} catch (error) {
res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json({ error: ErrorMessages.InternalError });
}
});
cusRouter.post("", handlePostCustomerRequest);
// cusRouter.put("", async (req: any, res: any) => {
// try {
// const { id, name, email, fingerprint, reset_at } = req.body;
// if (!id && !email) {
// throw new RecaseError({
// message: "Customer ID or email is required",
// code: ErrCode.InvalidCustomer,
// statusCode: StatusCodes.BAD_REQUEST,
// });
// }
// let existingCustomers = await CusService.getByIdOrEmail({
// sb: req.sb,
// id,
// email,
// orgId: req.orgId,
// env: req.env,
// });
// if (existingCustomers.length > 1) {
// throw new RecaseError({
// message: "Multiple customers found",
// code: ErrCode.MultipleCustomersFound,
// statusCode: StatusCodes.CONFLICT,
// });
// }
// let newCustomer: Customer;
// if (existingCustomers.length == 1) {
// const existing = existingCustomers[0];
// newCustomer = await CusService.update({
// sb: req.sb,
// internalCusId: existing.internal_id,
// update: { id, name, email, fingerprint },
// });
// } else {
// newCustomer = await createNewCustomer({
// sb: req.sb,
// orgId: req.orgId,
// env: req.env,
// customer: {
// id,
// name: name || "",
// email: email || "",
// fingerprint,
// },
// nextResetAt: reset_at,
// logger: req.logtail,
// });
// }
// res.status(200).json({
// customer: CustomerResponseSchema.parse(newCustomer),
// success: true,
// action: existingCustomers.length == 1 ? "update" : "create",
// });
// } catch (error) {
// handleRequestError({ req, error, res, action: "update customer" });
// }
// });
// BY CUSTOMER ID
cusRouter.get("/:customer_id", async (req: any, res: any) => {
try {
let customerId = req.params.customer_id;
let customer = await CusService.getById({
sb: req.sb,
id: customerId,
orgId: req.orgId,
env: req.env,
logger: req.logtail,
});
if (!customer) {
req.logtail.warn(
`GET /customers/${customerId}: not found | Org: ${req.minOrg.slug}`
);
res.status(StatusCodes.NOT_FOUND).json({
message: `Customer ${customerId} not found`,
code: ErrCode.CustomerNotFound,
});
return;
}
const { main, addOns, balances, invoices } = await getCustomerDetails({
customer,
sb: req.sb,
orgId: req.orgId,
env: req.env,
// params: req.query,
logger: req.logtail,
});
res.status(200).json({
customer: CustomerResponseSchema.parse(customer),
products: main,
add_ons: addOns,
entitlements: balances,
invoices,
});
} catch (error) {
handleRequestError({ req, error, res, action: "get customer" });
}
});
cusRouter.delete("/:customer_id", async (req: any, res: any) => {
try {
const data = await deleteCusById({
sb: req.sb,
minOrg: req.minOrg,
customerId: req.params.customer_id,
env: req.env,
logger: req.logtail,
});
res.status(200).json(data);
} catch (error) {
handleRequestError({ req, error, res, action: "delete customer" });
}
});
cusRouter.get("/:customer_id/events", async (req: any, res: any) => {
const customerId = req.params.customer_id;
try {
const events = await EventService.getByCustomerId({
sb: req.sb,
customerId,
orgId: req.orgId,
env: req.env,
});
for (const event of events) {
if (!event.value) {
delete event.value;
}
}
res.status(200).json({ events });
} catch (error) {
handleRequestError({ req, error, res, action: "get customer events" });
}
});
cusRouter.post("/:customer_id", async (req: any, res: any) => {
try {
const customerId = req.params.customer_id;
const [originalCustomer, org] = await Promise.all([
CusService.getByIdOrInternalId({
sb: req.sb,
idOrInternalId: customerId,
orgId: req.orgId,
env: req.env,
}),
OrgService.getFullOrg({ sb: req.sb, orgId: req.orgId }),
]);
if (!originalCustomer) {
throw new RecaseError({
message: `Update customer: Customer ${customerId} not found`,
code: ErrCode.CustomerNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
let newCusData: any = CreateCustomerSchema.parse(req.body);
if (notNullish(newCusData.id) && originalCustomer.id !== newCusData.id) {
// Fetch for existing customer
const existingCustomer = await CusService.getById({
sb: req.sb,
id: newCusData.id,
orgId: req.orgId,
env: req.env,
logger: req.logtail,
});
if (existingCustomer) {
throw new RecaseError({
message: `Update customer: Customer ${newCusData.id} already exists, can't change to this ID`,
code: ErrCode.DuplicateCustomerId,
statusCode: StatusCodes.CONFLICT,
});
}
} else {
delete newCusData.id;
}
// 2. Check if customer email is being changed
let stripeUpdate = {
email:
originalCustomer.email !== newCusData.email
? newCusData.email
: undefined,
name:
originalCustomer.name !== newCusData.name ? newCusData.name : undefined,
};
if (
Object.keys(stripeUpdate).length > 0 &&
originalCustomer.processor?.id
) {
const stripeCli = createStripeCli({ org, env: req.env });
await stripeCli.customers.update(
originalCustomer.processor.id,
stripeUpdate as any
);
}
const updatedCustomer = await CusService.update({
sb: req.sb,
internalCusId: originalCustomer.internal_id,
update: newCusData,
});
res.status(200).json({ customer: updatedCustomer });
} catch (error) {
handleRequestError({ req, error, res, action: "update customer" });
}
});
// Update customer entitlement directly
cusRouter.post(
"/customer_entitlements/:customer_entitlement_id",
handleUpdateEntitlement
);
cusRouter.post("/:customer_id/balances", handleUpdateBalances);
cusRouter.post(
"/customer_products/:customer_product_id",
handleCusProductExpired
);
cusRouter.get("/:customer_id/billing_portal", async (req: any, res: any) => {
try {
const customerId = req.params.customer_id;
const customer = await CusService.getById({
sb: req.sb,
id: customerId,
orgId: req.orgId,
env: req.env,
logger: req.logtail,
});
if (!customer) {
throw new RecaseError({
message: `Customer ${customerId} not found`,
code: ErrCode.CustomerNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
if (!customer.processor?.id) {
throw new RecaseError({
message: `Customer ${customerId} not connected to Stripe`,
code: ErrCode.InvalidRequest,
statusCode: StatusCodes.BAD_REQUEST,
});
}
const org = await OrgService.getFullOrg({ sb: req.sb, orgId: req.orgId });
const stripeCli = createStripeCli({ org, env: req.env });
const portal = await stripeCli.billingPortal.sessions.create({
customer: customer.processor.id,
return_url: org.stripe_config.success_url,
});
res.status(200).json({
url: portal.url,
});
} catch (error) {
handleRequestError({ req, error, res, action: "get billing portal" });
}
});
// Entitlements
// cusRouter.get("/:customer_id/entitlements", async (req: any, res: any) => {
// const customerId = req.params.customer_id;
// let balances: any[] = [];
// const customer = await CusService.getById({
// sb: req.sb,
// id: customerId,
// orgId: req.orgId,
// env: req.env,
// logger: req.logtail,
// });
// const fullCusProducts = await CusService.getFullCusProducts({
// sb: req.sb,
// internalCustomerId: customer.internal_id,
// withPrices: true,
// logger: req.logtail,
// });
// const cusEntsWithCusProduct = fullCusProductToCusEnts(fullCusProducts);
// const cusPrices = fullCusProductToCusPrices(fullCusProducts);
// balances = await getCusBalancesByEntitlement({
// cusEntsWithCusProduct: cusEntsWithCusProduct as any,
// cusPrices,
// entities,
// });
// for (const balance of balances) {
// if (balance.total && balance.balance) {
// balance.used = balance.total - balance.balance;
// delete balance.total;
// }
// }
// res.status(200).json(balances);
// });
// Invoice
cusRouter.post("/:customer_id/coupons/:coupon_id", handleAddCouponToCus);
cusRouter.use("/:customer_id/entities", entityRouter);