fix: entity_id now appears in customer products, added test for it

This commit is contained in:
John Yeo
2025-06-18 10:45:15 +01:00
parent 81e45edb74
commit 3b1cff357b
17 changed files with 133 additions and 48 deletions

View File

@@ -2,6 +2,13 @@
![Autumn](assets/github_hero.png)
![Discord](https://img.shields.io/badge/Join%20Community-5865F2?logo=discord&logoColor=white)
![Follow](https://img.shields.io/twitter/follow/autumn_pricing?style=social)
![Y Combinator](https://img.shields.io/badge/Y%20Combinator-F24-orange)
[![Cloud](https://img.shields.io/badge/Cloud-☁️-blue)](https://app.useautumn.com)
[![Documentation](https://img.shields.io/badge/Documentation-📕-blue)](https://docs.useautumn.com)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/useautumn/autumn)
[Autumn](https://useautumn.com) is an open-source layer between Stripe and your application, allowing you to create any pricing model and embed it with a couple lines of code. On Autumn you can build:
- Subscriptions
- Credit systems & top ups

Binary file not shown.

Before

Width:  |  Height:  |  Size: 690 KiB

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -7,7 +7,8 @@ fi
# Set default subdomain if env var not found
if [ -z "$LOCALTUNNEL_RESERVED_KEY" ]; then
LOCALTUNNEL_RESERVED_KEY="abcasdjnaslkjdnas"
echo "No LOCALTUNNEL_RESERVED_KEY found in .env, exiting..."
exit 0
fi
echo "LOCALTUNNEL_RESERVED_KEY: ${LOCALTUNNEL_RESERVED_KEY}"

View File

@@ -195,7 +195,6 @@ stripeWebhookRouter.post(
logger,
res: response,
});
return;
break;
}
} catch (error) {

View File

@@ -67,9 +67,6 @@ export async function handleCusDiscountDeleted({
return;
}
// Send response first...?
res.status(200).json({ message: "OK" });
const reward = await RewardService.get({
db,
orgId: org.id,

View File

@@ -101,7 +101,11 @@ adminRouter.get("/orgs", async (req: any, res: any) => {
.where(
and(
search
? or(ilike(organizations.name, `%${search as string}%`))
? or(
ilike(organizations.name, `%${search as string}%`),
ilike(organizations.id, `%${search as string}%`),
ilike(organizations.slug, `%${search as string}%`),
)
: undefined,
after
? or(
@@ -143,7 +147,7 @@ adminRouter.get("/orgs", async (req: any, res: any) => {
...org,
users: memberships
.filter((membership) => membership.member.organizationId === org.id)
.map((membership) => membership.user?.email),
.map((membership) => membership.user),
})),
hasNextPage: orgs.length > 20,
});

View File

@@ -413,6 +413,7 @@ export const processFullCusProduct = ({
stripe_subscription_ids: cusProduct.subscription_ids || [],
started_at: cusProduct.starts_at,
// entity_id: cusProduct.entity_id,
entity_id: cusProduct.internal_entity_id
? entities?.find((e) => e.internal_id == cusProduct.internal_entity_id)
?.id

View File

@@ -32,7 +32,8 @@ export const handleGetCustomer = async (req: any, res: any) =>
CusProductStatus.PastDue,
CusProductStatus.Scheduled,
],
withEntities: expandArray.includes(CusExpand.Entities),
withEntities: true,
// withEntities: expandArray.includes(CusExpand.Entities),
expand: expandArray,
allowNotFound: true,
}),

View File

@@ -11,6 +11,7 @@ import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
const testCase = "aentity1";
@@ -89,5 +90,12 @@ describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach to entity via
env,
entityId,
});
let customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
product: pro,
entityId,
});
});
});

View File

@@ -1,12 +1,11 @@
import { Toaster } from "@/components/ui/sonner";
import { cn } from "@/lib/utils";
import { useEnv } from "@/utils/envUtils";
import { getRedirectUrl, navigateTo } from "@/utils/genUtils";
import { navigateTo } from "@/utils/genUtils";
import LoadingScreen from "@/views/general/LoadingScreen";
import { MainSidebar } from "@/views/main-sidebar/MainSidebar";
import { AppEnv } from "@autumn/shared";
import { useEffect } from "react";
import { Navigate, Outlet, useLocation, useNavigate } from "react-router";
import { Outlet, useNavigate } from "react-router";
import { usePostHog } from "posthog-js/react";
import { Button } from "@/components/ui/button";
@@ -19,8 +18,6 @@ export function MainLayout() {
const env = useEnv();
const { data, isPending } = useSession();
console.log("Session", data, "isPending", isPending);
const navigate = useNavigate();
const posthog = usePostHog();

View File

@@ -0,0 +1,21 @@
import { authClient } from "@/lib/auth-client";
import { toast } from "sonner";
export const impersonateUser = async (userId: string) => {
console.log("impersonating user", userId);
try {
await authClient.admin.stopImpersonating();
} catch (error) {
console.error(error);
}
const res = await authClient.admin.impersonateUser({
userId,
});
if (res.error) {
toast.error("Something went wrong");
return;
}
window.location.reload();
};

View File

@@ -0,0 +1,31 @@
import { Button } from "@/components/ui/button";
import { impersonateUser } from "../adminUtils";
import { User } from "better-auth";
import { useState } from "react";
import { toast } from "sonner";
export const ImpersonateButton = ({ userId }: { userId?: string }) => {
const [loading, setLoading] = useState(false);
if (!userId) {
return null;
}
return (
<Button
variant="outline"
size="sm"
onClick={async () => {
setLoading(true);
try {
await impersonateUser(userId);
} catch (error: any) {
toast.error(`Failed to impersonate user: ${error.message}`);
}
setLoading(false);
}}
shimmer={loading}
>
Impersonate
</Button>
);
};

View File

@@ -1,13 +1,15 @@
import { ColumnDef, Row } from "@tanstack/react-table";
import CopyButton from "@/components/general/CopyButton";
import { format } from "date-fns";
import { User } from "better-auth";
import { ImpersonateButton } from "./components/ImpersonateBtn";
export type Org = {
id: string;
name: string;
slug: string;
createdAt: string;
users: string[];
users: User[];
};
// Helper to add width and canCopy to columns
@@ -70,13 +72,31 @@ export const columns: OrgColumnDef[] = [
);
},
},
{
accessorKey: "impersonate",
header: "Impersonate",
width: 150,
cell: ({ row }: { row: Row<Org> }) => {
const users = row.getValue("users") as User[];
if (!users || users.length === 0) {
return null;
}
return <ImpersonateButton userId={users?.[0]?.id} />;
},
},
{
accessorKey: "users",
header: "Users",
width: "100%",
cell: ({ row }: { row: Row<Org> }) => {
const value = row.getValue("users");
return <span className="truncate">{(value as string[]).join(", ")}</span>;
return (
<span className="truncate">
{(value as User[]).map((user) => user.email)}
</span>
);
},
},
];

View File

@@ -4,6 +4,8 @@ import CopyButton from "@/components/general/CopyButton";
import { format } from "date-fns";
import { authClient } from "@/lib/auth-client";
import { toast } from "sonner";
import { impersonateUser } from "./adminUtils";
import { ImpersonateButton } from "./components/ImpersonateBtn";
export type User = {
id: string;
@@ -78,25 +80,7 @@ export const columns: UserColumnDef[] = [
header: "Impersonate",
width: "100%",
cell: ({ row }: { row: Row<User> }) => (
<Button
variant="outline"
size="sm"
onClick={async () => {
const res = await authClient.admin.impersonateUser({
userId: row.original.id,
});
if (res.error) {
toast.error("Something went wrong");
return;
}
window.location.reload();
}}
style={{ width: 100 }}
>
Impersonate
</Button>
<ImpersonateButton userId={row.original.id} />
),
enableSorting: false,
enableHiding: false,

View File

@@ -1,8 +1,6 @@
import { NavButton } from "./NavButton";
import { SidebarTop } from "./SidebarTop";
import { useEnv } from "@/utils/envUtils";
import SidebarBottom from "./SidebarBottom";
import { NavButton } from "./NavButton";
import { useEnv } from "@/utils/envUtils";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { SidebarContext } from "./SidebarContext";
@@ -53,14 +51,6 @@ export const MainSidebar = () => {
title="Developer"
env={env}
/>
{/* <AdminOnly>
<NavButton
value="admin"
icon={<Shield size={15} />}
title="Admin"
env={env}
/>
</AdminOnly> */}
</div>
</div>
{/* Sidebar bottom */}

View File

@@ -5,6 +5,7 @@ import { AppEnv } from "@autumn/shared";
import { Link } from "react-router";
import { useState } from "react";
import { useSidebarContext } from "./SidebarContext";
import { useEnv } from "@/utils/envUtils";
export const NavButton = ({
value,
@@ -18,12 +19,13 @@ export const NavButton = ({
value: string;
icon: any;
title: string;
env: AppEnv;
env?: AppEnv;
className?: string;
href?: string;
online?: boolean;
}) => {
// Get window path
env = useEnv();
const { state } = useSidebarContext();
const tab = useTab();

View File

@@ -25,7 +25,14 @@ import {
} from "@/lib/auth-client";
import { FrontendOrg, user } from "@autumn/shared";
import { DropdownMenuGroup } from "@radix-ui/react-dropdown-menu";
import { ChevronDown, LogOut, Plus, Settings, Trash } from "lucide-react";
import {
ChevronDown,
LogOut,
Plus,
Settings,
Shield,
Trash,
} from "lucide-react";
import React from "react";
import { useState } from "react";
import { CreateNewOrg } from "./CreateNewOrg";
@@ -38,6 +45,8 @@ import { useMemberships } from "../org-dropdown/hooks/useMemberships";
import { useSidebarContext } from "../SidebarContext";
import { OrgLogo } from "../org-dropdown/components/OrgLogo";
import { AdminHover } from "@/components/general/AdminHover";
import { NavButton } from "../NavButton";
import { AdminOnly } from "@/views/admin/components/AdminOnly";
export const OrgDropdown = () => {
const { org, isLoading, error } = useOrg();
@@ -108,6 +117,19 @@ export const OrgDropdown = () => {
align="start"
className="border-1 border-zinc-200 shadow-sm w-48"
>
<AdminOnly>
<DropdownMenuItem
onClick={() => {
window.location.href = "/admin";
}}
>
<div className="flex justify-between w-full items-center gap-2 text-t2">
Admin
<Shield size={12} />
</div>
</DropdownMenuItem>
<DropdownMenuSeparator />
</AdminOnly>
<DropdownMenuItem className="flex justify-between w-full items-center gap-2 text-t2">
<div className="flex flex-col">
<span>{session?.user?.name}</span>