created PUT customer and update customer balances
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border border-zinc-200 px-2.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-zinc-950 focus:ring-offset-2 dark:border-zinc-800 dark:focus:ring-zinc-300",
|
||||
@@ -17,13 +17,17 @@ const badgeVariants = cva(
|
||||
outline: "text-zinc-950 dark:text-zinc-50",
|
||||
purple: "bg-purple-50 text-purple-500",
|
||||
blue: "bg-blue-50 text-blue-500",
|
||||
|
||||
green: "bg-green-50 text-green-500 border-green-200",
|
||||
yellow: "bg-yellow-50 text-yellow-500 border-yellow-200",
|
||||
red: "bg-red-50 text-red-500 border-red-200",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
@@ -32,7 +36,7 @@ export interface BadgeProps
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants };
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import KSUID from "ksuid";
|
||||
|
||||
export const compareStatus = (statusA: string, statusB: string) => {
|
||||
const statusOrder = ["scheduled", "active", "past_due", "expired"];
|
||||
return statusOrder.indexOf(statusA) - statusOrder.indexOf(statusB);
|
||||
};
|
||||
|
||||
export const generateId = (prefix: string) => {
|
||||
if (!prefix) {
|
||||
return KSUID.randomSync().string;
|
||||
|
||||
13
frontend/src/views/customers/StatusBadge.tsx
Normal file
13
frontend/src/views/customers/StatusBadge.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { CusProductStatus } from "@autumn/shared";
|
||||
|
||||
export const StatusBadge = ({ status }: { status: string }) => {
|
||||
const statusToVariant = {
|
||||
[CusProductStatus.Active]: "green",
|
||||
[CusProductStatus.Scheduled]: "blue",
|
||||
[CusProductStatus.PastDue]: "yellow",
|
||||
[CusProductStatus.Expired]: "red",
|
||||
};
|
||||
|
||||
return <Badge variant={statusToVariant[status]}>{status}</Badge>;
|
||||
};
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { formatUnixToDateTimeString } from "@/utils/formatUtils/formatDateUtils";
|
||||
import { navigateTo } from "@/utils/genUtils";
|
||||
import { compareStatus, navigateTo } from "@/utils/genUtils";
|
||||
import { CusProduct } from "@autumn/shared";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCustomerContext } from "./CustomerContext";
|
||||
import { StatusBadge } from "../StatusBadge";
|
||||
|
||||
export const CustomerProductList = ({
|
||||
customer,
|
||||
@@ -20,7 +21,16 @@ export const CustomerProductList = ({
|
||||
products: any;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const {env} = useCustomerContext();
|
||||
const { env } = useCustomerContext();
|
||||
|
||||
const sortedProducts = customer.products.sort((a: any, b: any) => {
|
||||
if (a.status !== b.status) {
|
||||
return compareStatus(a.status, b.status);
|
||||
}
|
||||
|
||||
// return a.product.name.localeCompare(b.product.name);
|
||||
return b.created_at - a.created_at;
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -36,7 +46,7 @@ export const CustomerProductList = ({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{customer.products.map((cusProduct: CusProduct) => {
|
||||
{sortedProducts.map((cusProduct: CusProduct) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={cusProduct.id}
|
||||
@@ -55,7 +65,9 @@ export const CustomerProductList = ({
|
||||
<TableCell className="max-w-[100px] overflow-hidden text-ellipsis">
|
||||
{cusProduct.product_id}
|
||||
</TableCell>
|
||||
<TableCell>{cusProduct.status}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={cusProduct.status} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatUnixToDateTimeString(cusProduct.created_at)}
|
||||
</TableCell>
|
||||
|
||||
@@ -14,6 +14,9 @@ import {
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { useCustomerContext } from "../CustomerContext";
|
||||
import { formatUnixToDateTimeString } from "@/utils/formatUtils/formatDateUtils";
|
||||
import { compareStatus } from "@/utils/genUtils";
|
||||
import { StatusBadge } from "../../StatusBadge";
|
||||
|
||||
export const CustomerEntitlementsList = ({ customer }: { customer: any }) => {
|
||||
const { products } = useCustomerContext();
|
||||
@@ -28,6 +31,30 @@ export const CustomerEntitlementsList = ({ customer }: { customer: any }) => {
|
||||
return product?.name;
|
||||
};
|
||||
|
||||
const sortedEntitlements = customer.entitlements.sort((a: any, b: any) => {
|
||||
const statusA = customer.products.find(
|
||||
(cp: any) => cp.id === a.customer_product_id
|
||||
)?.status;
|
||||
|
||||
const statusB = customer.products.find(
|
||||
(cp: any) => cp.id === b.customer_product_id
|
||||
)?.status;
|
||||
|
||||
if (statusA !== statusB) {
|
||||
return compareStatus(statusA, statusB);
|
||||
}
|
||||
|
||||
const productA = customer.products.find(
|
||||
(cp: any) => cp.id === a.customer_product_id
|
||||
);
|
||||
|
||||
const productB = customer.products.find(
|
||||
(cp: any) => cp.id === b.customer_product_id
|
||||
);
|
||||
|
||||
return productA.product.name.localeCompare(productB.product.name);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table className="p-2">
|
||||
@@ -36,11 +63,12 @@ export const CustomerEntitlementsList = ({ customer }: { customer: any }) => {
|
||||
<TableHead className="w-[150px]">Product</TableHead>
|
||||
<TableHead className="w-[150px]">Feature</TableHead>
|
||||
<TableHead className="">Balance</TableHead>
|
||||
{/* <TableHead className="w-[100px]"></TableHead> */}
|
||||
<TableHead className="">Next Reset</TableHead>
|
||||
<TableHead className="">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{customer.entitlements.map((cusEnt: FullCustomerEntitlement) => {
|
||||
{sortedEntitlements.map((cusEnt: FullCustomerEntitlement) => {
|
||||
const entitlement = cusEnt.entitlement;
|
||||
const allowanceType = entitlement.allowance_type;
|
||||
return (
|
||||
@@ -58,11 +86,18 @@ export const CustomerEntitlementsList = ({ customer }: { customer: any }) => {
|
||||
? "None"
|
||||
: cusEnt.balance}
|
||||
</TableCell>
|
||||
{/* <TableCell className="flex justify-end">
|
||||
<CustomerEntitlementToolbar
|
||||
entitlement={cusEnt.entitlement}
|
||||
/>
|
||||
</TableCell> */}
|
||||
<TableCell>
|
||||
{formatUnixToDateTimeString(cusEnt.next_reset_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge
|
||||
status={
|
||||
customer.products.find(
|
||||
(p: any) => p.id === cusEnt.customer_product_id
|
||||
)?.status
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -48,7 +48,7 @@ export const EntitlementConfig = ({
|
||||
const [fields, setFields] = useState({
|
||||
allowance_type: entitlement?.allowance_type || AllowanceType.Fixed,
|
||||
allowance: entitlement?.allowance || 0,
|
||||
interval: entitlement?.interval || EntInterval.Minute,
|
||||
interval: entitlement?.interval || EntInterval.Month,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Run current file
|
||||
# npx tsx scripts/alex.ts
|
||||
npx tsx scripts/copyOrg.ts
|
||||
npx tsx scripts/alex_test.ts
|
||||
|
||||
@@ -6,7 +6,14 @@ import RecaseError, {
|
||||
handleRequestError,
|
||||
} from "@/utils/errorUtils.js";
|
||||
import { generateId } from "@/utils/genUtils.js";
|
||||
import { CreateCustomerSchema, Customer, ProcessorType } from "@autumn/shared";
|
||||
import {
|
||||
AppEnv,
|
||||
CreateCustomer,
|
||||
CreateCustomerSchema,
|
||||
Customer,
|
||||
CustomerResponseSchema,
|
||||
ProcessorType,
|
||||
} from "@autumn/shared";
|
||||
import { Router } from "express";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { CusService } from "../../customers/CusService.js";
|
||||
@@ -19,25 +26,88 @@ import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
|
||||
import { EventService } from "../events/EventService.js";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { CustomerEntitlementService } from "@/internal/customers/entitlements/CusEntitlementService.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
|
||||
export const cusRouter = Router();
|
||||
|
||||
const createNewCustomer = async ({
|
||||
sb,
|
||||
orgId,
|
||||
env,
|
||||
customer,
|
||||
nextResetAt,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
customer: CreateCustomer;
|
||||
nextResetAt?: number;
|
||||
}) => {
|
||||
const org = await OrgService.getFullOrg({
|
||||
sb,
|
||||
orgId,
|
||||
});
|
||||
|
||||
const customerData: Customer = {
|
||||
...customer,
|
||||
internal_id: generateId("cus"),
|
||||
org_id: orgId,
|
||||
created_at: Date.now(),
|
||||
env,
|
||||
};
|
||||
|
||||
let stripeCustomer: Stripe.Customer | undefined;
|
||||
if (org.stripe_connected) {
|
||||
stripeCustomer = await createStripeCustomer({
|
||||
org,
|
||||
env,
|
||||
customer: customerData,
|
||||
});
|
||||
customerData.processor = {
|
||||
type: ProcessorType.Stripe,
|
||||
id: stripeCustomer?.id,
|
||||
};
|
||||
}
|
||||
|
||||
const newCustomer = await CusService.createCustomer({
|
||||
sb,
|
||||
customer: customerData,
|
||||
});
|
||||
|
||||
// Attach default product to customer
|
||||
const defaultProds = await ProductService.getFullDefaultProduct({
|
||||
sb,
|
||||
orgId,
|
||||
env,
|
||||
});
|
||||
|
||||
for (const product of defaultProds) {
|
||||
await createFullCusProduct({
|
||||
sb,
|
||||
attachParams: {
|
||||
org,
|
||||
customer: newCustomer,
|
||||
product,
|
||||
prices: product.prices,
|
||||
entitlements: product.entitlements,
|
||||
freeTrial: null, // TODO: Free trial not supported on default product yet
|
||||
optionsList: [],
|
||||
},
|
||||
nextResetAt,
|
||||
});
|
||||
}
|
||||
|
||||
return newCustomer;
|
||||
};
|
||||
|
||||
cusRouter.post("", async (req: any, res: any) => {
|
||||
try {
|
||||
const data = req.body;
|
||||
const org = await OrgService.getFullOrg({ sb: req.sb, orgId: req.orgId });
|
||||
|
||||
// 1. Validate data
|
||||
try {
|
||||
CreateCustomerSchema.parse(data);
|
||||
} catch (error: any) {
|
||||
throw new RecaseError({
|
||||
message: "Invalid customer data, error: " + formatZodError(error),
|
||||
code: ErrCode.InvalidCustomer,
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
CreateCustomerSchema.parse(data);
|
||||
|
||||
// 2. Check if customer ID already exists
|
||||
const existingCustomer = await CusService.getCustomer({
|
||||
@@ -55,95 +125,19 @@ cusRouter.post("", async (req: any, res: any) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Create stripe customer
|
||||
let stripeCustomer: Stripe.Customer | undefined;
|
||||
if (org.stripe_connected) {
|
||||
stripeCustomer = await createStripeCustomer({
|
||||
org,
|
||||
env: req.env,
|
||||
customer: data,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Create customer in db
|
||||
const newCustomer: Customer = {
|
||||
...data,
|
||||
internal_id: generateId("cus"),
|
||||
org_id: req.orgId,
|
||||
created_at: Date.now(),
|
||||
const createdCustomer = await createNewCustomer({
|
||||
sb: req.sb,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
|
||||
processor: stripeCustomer && {
|
||||
type: ProcessorType.Stripe,
|
||||
id: stripeCustomer.id,
|
||||
},
|
||||
};
|
||||
|
||||
let createdCustomer: Customer;
|
||||
try {
|
||||
createdCustomer = await CusService.createCustomer({
|
||||
sb: req.sb,
|
||||
customer: newCustomer,
|
||||
});
|
||||
} catch (error: any) {
|
||||
throw new RecaseError({
|
||||
message: "Error creating customer",
|
||||
code: ErrCode.CreateCustomerFailed,
|
||||
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Attach default product to customer
|
||||
try {
|
||||
const defaultProduct = await ProductService.getFullDefaultProduct({
|
||||
sb: req.sb,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
});
|
||||
|
||||
if (defaultProduct) {
|
||||
const org = await OrgService.getFullOrg({
|
||||
sb: req.sb,
|
||||
orgId: req.orgId,
|
||||
});
|
||||
await createFullCusProduct({
|
||||
sb: req.sb,
|
||||
attachParams: {
|
||||
org,
|
||||
customer: createdCustomer,
|
||||
product: defaultProduct,
|
||||
prices: defaultProduct.prices,
|
||||
entitlements: defaultProduct.entitlements,
|
||||
freeTrial: null,
|
||||
optionsList: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new RecaseError({
|
||||
message: "Error attaching default product to customer",
|
||||
code: ErrCode.AttachProductToCustomerFailed,
|
||||
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({ customer: createdCustomer, success: true });
|
||||
} catch (error: any) {
|
||||
if (error instanceof RecaseError) {
|
||||
error.print();
|
||||
res
|
||||
.status(error.statusCode)
|
||||
.json({ message: error.message, code: error.code });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Unknown error creating customer", error);
|
||||
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
||||
error: ErrorMessages.InternalError,
|
||||
code: ErrCode.InternalError,
|
||||
customer: data,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
customer: CustomerResponseSchema.parse(createdCustomer),
|
||||
success: true,
|
||||
});
|
||||
} catch (error: any) {
|
||||
handleRequestError({ error, res, action: "create customer" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -225,3 +219,142 @@ cusRouter.get("/:customer_id/events", async (req: any, res: any) => {
|
||||
handleRequestError({ error, res, action: "get customer events" });
|
||||
}
|
||||
});
|
||||
|
||||
cusRouter.put("", async (req: any, res: any) => {
|
||||
const { id, name, email, fingerprint, next_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 existing = await CusService.getByIdOrEmail({
|
||||
sb: req.sb,
|
||||
id,
|
||||
email,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
});
|
||||
|
||||
let newCustomer: Customer;
|
||||
if (existing) {
|
||||
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,
|
||||
email,
|
||||
fingerprint,
|
||||
},
|
||||
nextResetAt: next_reset_at,
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
customer: CustomerResponseSchema.parse(newCustomer),
|
||||
success: true,
|
||||
action: existing ? "update" : "create",
|
||||
});
|
||||
});
|
||||
|
||||
cusRouter.post("/:customer_id/balances", async (req: any, res: any) => {
|
||||
try {
|
||||
const cusId = req.params.customer_id;
|
||||
const { balances } = req.body;
|
||||
|
||||
const customer = await CusService.getById({
|
||||
sb: req.sb,
|
||||
id: cusId,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
});
|
||||
|
||||
if (!customer) {
|
||||
throw new RecaseError({
|
||||
message: `Customer ${cusId} not found`,
|
||||
code: ErrCode.CustomerNotFound,
|
||||
statusCode: StatusCodes.NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
const features = await FeatureService.getFromReq(req);
|
||||
const featuresToUpdate = features.filter((f) =>
|
||||
balances.map((b: any) => b.feature_id).includes(f.id)
|
||||
);
|
||||
|
||||
const cusEnts = await CustomerEntitlementService.getActiveInFeatureIds({
|
||||
sb: req.sb,
|
||||
internalCustomerId: customer.internal_id,
|
||||
internalFeatureIds: featuresToUpdate.map((f) => f.internal_id),
|
||||
});
|
||||
|
||||
// console.log("cusEnts", cusEnts);
|
||||
for (const balance of balances) {
|
||||
if (!balance.feature_id) {
|
||||
throw new RecaseError({
|
||||
message: "Feature ID is required",
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof balance.balance !== "number") {
|
||||
throw new RecaseError({
|
||||
message: "Balance must be a number",
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
});
|
||||
}
|
||||
|
||||
const feature = featuresToUpdate.find((f) => f.id === balance.feature_id);
|
||||
// const cusEntToUpdate = cusEnts.find((e) => e.feature_id === feature.id);
|
||||
|
||||
// How much to update
|
||||
let curBalance = 0;
|
||||
let newBalance = balance.balance;
|
||||
for (const cusEnt of cusEnts) {
|
||||
if (cusEnt.internal_feature_id === feature.internal_id) {
|
||||
curBalance += cusEnt.balance;
|
||||
}
|
||||
}
|
||||
|
||||
let updateAmount = newBalance - curBalance;
|
||||
|
||||
for (const cusEnt of cusEnts) {
|
||||
if (updateAmount == 0) break;
|
||||
if (cusEnt.internal_feature_id === feature.internal_id) {
|
||||
if (cusEnt.balance + updateAmount < 0) {
|
||||
updateAmount += cusEnt.balance;
|
||||
newBalance = 0;
|
||||
} else {
|
||||
newBalance = cusEnt.balance + updateAmount;
|
||||
updateAmount = 0;
|
||||
}
|
||||
|
||||
await CustomerEntitlementService.update({
|
||||
sb: req.sb,
|
||||
id: cusEnt.id,
|
||||
updates: {
|
||||
balance: newBalance,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
handleRequestError({ error, res, action: "update customer balances" });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,6 +5,109 @@ import { ErrCode } from "@/errors/errCodes.js";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
export class CusService {
|
||||
static async getById({
|
||||
sb,
|
||||
id,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
id: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("customers")
|
||||
.select()
|
||||
.eq("id", id)
|
||||
.eq("org_id", orgId)
|
||||
.eq("env", env);
|
||||
|
||||
if (error) {
|
||||
throw new RecaseError({
|
||||
code: ErrCode.InternalError,
|
||||
message: "Failed to get customer by ID",
|
||||
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return data[0];
|
||||
}
|
||||
|
||||
static async getByEmail({
|
||||
sb,
|
||||
email,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
email: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("customers")
|
||||
.select()
|
||||
.eq("email", email);
|
||||
|
||||
if (error) {
|
||||
throw new RecaseError({
|
||||
code: ErrCode.InternalError,
|
||||
message: "Failed to get customer by email",
|
||||
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return null;
|
||||
} else if (data.length > 2) {
|
||||
throw new RecaseError({
|
||||
code: ErrCode.InternalError,
|
||||
message: "Multiple customers found with the same email",
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
});
|
||||
}
|
||||
|
||||
return data[0];
|
||||
}
|
||||
|
||||
static async getByIdOrEmail({
|
||||
sb,
|
||||
id,
|
||||
email,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
sb: SupabaseClient;
|
||||
id: string;
|
||||
email: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("customers")
|
||||
.select()
|
||||
.or(`id.eq.${id},email.eq.${email}`)
|
||||
.eq("org_id", orgId)
|
||||
.eq("env", env)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
if (error.code === "PGRST116") {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static async getByInternalId({
|
||||
sb,
|
||||
internalId,
|
||||
@@ -199,9 +302,18 @@ export class CusService {
|
||||
const { data, error } = await sb
|
||||
.from("customers")
|
||||
.update(update)
|
||||
.eq("internal_id", internalCusId);
|
||||
.eq("internal_id", internalCusId)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
if (error.code == "2305") {
|
||||
throw new RecaseError({
|
||||
message: `Customer ${internalCusId} already exists`,
|
||||
code: ErrCode.DuplicateCustomerId,
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
});
|
||||
}
|
||||
throw new RecaseError({
|
||||
message: `Error updating customer...please try again later.`,
|
||||
code: ErrCode.InternalError,
|
||||
|
||||
@@ -72,6 +72,10 @@ export const initCusEntitlement = ({
|
||||
|
||||
// 3. Define expires at (TODO next time...)
|
||||
let isBooleanFeature = entitlement.feature.type === FeatureType.Boolean;
|
||||
let nextResetNull =
|
||||
isBooleanFeature ||
|
||||
entitlement.allowance_type === AllowanceType.Unlimited ||
|
||||
entitlement.interval == EntInterval.Lifetime;
|
||||
|
||||
return {
|
||||
id: generateId("cus_ent"),
|
||||
@@ -91,9 +95,7 @@ export const initCusEntitlement = ({
|
||||
: entitlement.allowance_type === AllowanceType.Unlimited,
|
||||
balance: isBooleanFeature ? null : balance,
|
||||
usage_allowed: isBooleanFeature ? null : false,
|
||||
next_reset_at: isBooleanFeature
|
||||
? null
|
||||
: nextResetAt || nextResetAtCalculated,
|
||||
next_reset_at: nextResetNull ? null : nextResetAt || nextResetAtCalculated,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { AppEnv, CustomerEntitlement, ErrCode } from "@autumn/shared";
|
||||
import { CustomerEntitlement, ErrCode } from "@autumn/shared";
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
|
||||
@@ -132,7 +132,8 @@ export class CustomerEntitlementService {
|
||||
)
|
||||
.eq("internal_customer_id", internalCustomerId)
|
||||
.in("internal_feature_id", internalFeatureIds)
|
||||
.eq("customer_product.status", "active");
|
||||
.eq("customer_product.status", "active")
|
||||
.order("created_at", { ascending: true });
|
||||
|
||||
if (error) {
|
||||
throw new RecaseError({
|
||||
|
||||
@@ -32,19 +32,22 @@ export class ProductService {
|
||||
}) {
|
||||
const { data, error } = await sb
|
||||
.from("products")
|
||||
.select("*, prices(*), entitlements(*, feature:features(*))")
|
||||
.select(
|
||||
"*, prices(*), entitlements(*, feature:features(*)), free_trial:free_trials(*)"
|
||||
)
|
||||
.eq("org_id", orgId)
|
||||
.eq("env", env)
|
||||
.eq("is_default", true)
|
||||
.single();
|
||||
.eq("is_default", true);
|
||||
|
||||
if (error) {
|
||||
if (error.code === "PGRST116") {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
for (const product of data) {
|
||||
product.free_trial =
|
||||
product.free_trial.length > 0 ? product.free_trial[0] : null;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,26 +2,24 @@ import { z } from "zod";
|
||||
import { AppEnv } from "../genModels.js";
|
||||
|
||||
export const CustomerSchema = z.object({
|
||||
id: z.string(), // given by user
|
||||
name: z.string().nullish(),
|
||||
email: z.string().nullish(),
|
||||
fingerprint: z.string().nullish(),
|
||||
|
||||
// Internal
|
||||
internal_id: z.string(),
|
||||
org_id: z.string(),
|
||||
created_at: z.number(),
|
||||
env: z.nativeEnum(AppEnv),
|
||||
processor: z.any(),
|
||||
|
||||
id: z.string(), // given by user
|
||||
|
||||
name: z.string().nullish(),
|
||||
email: z.string().nullish(),
|
||||
fingerprint: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const CreateCustomerSchema = CustomerSchema.omit({
|
||||
internal_id: true,
|
||||
org_id: true,
|
||||
created_at: true,
|
||||
env: true,
|
||||
processor: true,
|
||||
fingerprint: true,
|
||||
export const CreateCustomerSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().default(""),
|
||||
email: z.string().default(""),
|
||||
fingerprint: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const CustomerDataSchema = z.object({
|
||||
@@ -30,5 +28,15 @@ export const CustomerDataSchema = z.object({
|
||||
fingerprint: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const CustomerResponseSchema = CustomerSchema.omit({
|
||||
// created_at: true,
|
||||
// env: true,
|
||||
internal_id: true,
|
||||
org_id: true,
|
||||
processor: true,
|
||||
});
|
||||
|
||||
export type Customer = z.infer<typeof CustomerSchema>;
|
||||
export type CustomerData = z.infer<typeof CustomerDataSchema>;
|
||||
export type CustomerResponse = z.infer<typeof CustomerResponseSchema>;
|
||||
export type CreateCustomer = z.infer<typeof CreateCustomerSchema>;
|
||||
|
||||
Reference in New Issue
Block a user