Merge branch 'pricing-plan'
# Conflicts: # web/bun.lockb # web/drizzle/meta/_journal.json # web/src/db/schema.ts
This commit is contained in:
commit
14c3ca6bf5
@ -248,4 +248,4 @@
|
|||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
"@hono/zod-openapi": "^0.9.5",
|
"@hono/zod-openapi": "^0.9.5",
|
||||||
"@hono/zod-validator": "^0.1.11",
|
"@hono/zod-validator": "^0.1.11",
|
||||||
"@hookform/resolvers": "^3.3.2",
|
"@hookform/resolvers": "^3.3.2",
|
||||||
|
"@lemonsqueezy/lemonsqueezy.js": "^1.2.5",
|
||||||
"@mdx-js/loader": "^3.0.0",
|
"@mdx-js/loader": "^3.0.0",
|
||||||
"@mdx-js/react": "^3.0.0",
|
"@mdx-js/react": "^3.0.0",
|
||||||
"@neondatabase/serverless": "^0.6.0",
|
"@neondatabase/serverless": "^0.6.0",
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import { parseDataSafe } from "../../../../lib/parseDataSafe";
|
import { parseDataSafe } from "../../../../lib/parseDataSafe";
|
||||||
import { db } from "@/db/db";
|
import { db } from "@/db/db";
|
||||||
import { workflowRunOutputs, workflowRunsTable } from "@/db/schema";
|
import {
|
||||||
|
userUsageTable,
|
||||||
|
workflowRunOutputs,
|
||||||
|
workflowRunsTable,
|
||||||
|
workflowTable,
|
||||||
|
} from "@/db/schema";
|
||||||
|
import { getDuration } from "@/lib/getRelativeTime";
|
||||||
|
import { getSubscription, setUsage } from "@/server/linkToPricing";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -27,7 +34,6 @@ export async function POST(request: Request) {
|
|||||||
data: output_data,
|
data: output_data,
|
||||||
});
|
});
|
||||||
} else if (status) {
|
} else if (status) {
|
||||||
// console.log("status", status);
|
|
||||||
const workflow_run = await db
|
const workflow_run = await db
|
||||||
.update(workflowRunsTable)
|
.update(workflowRunsTable)
|
||||||
.set({
|
.set({
|
||||||
@ -35,8 +41,15 @@ export async function POST(request: Request) {
|
|||||||
ended_at:
|
ended_at:
|
||||||
status === "success" || status === "failed" ? new Date() : null,
|
status === "success" || status === "failed" ? new Date() : null,
|
||||||
})
|
})
|
||||||
.where(eq(workflowRunsTable.id, run_id))
|
.where(eq(workflowRunsTable.id, run_id));
|
||||||
.returning();
|
|
||||||
|
// get data from workflowRunsTable
|
||||||
|
const userUsageTime = await importUserUsageData(run_id);
|
||||||
|
|
||||||
|
if (userUsageTime) {
|
||||||
|
// get the usage_time from userUsage
|
||||||
|
await addSubscriptionUnit(userUsageTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// const workflow_version = await db.query.workflowVersionTable.findFirst({
|
// const workflow_version = await db.query.workflowVersionTable.findFirst({
|
||||||
@ -54,3 +67,47 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function addSubscriptionUnit(userUsageTime: number) {
|
||||||
|
const subscription = await getSubscription();
|
||||||
|
|
||||||
|
// round up userUsageTime to the nearest integer
|
||||||
|
const roundedUsageTime = Math.ceil(userUsageTime);
|
||||||
|
|
||||||
|
if (subscription) {
|
||||||
|
const usage = await setUsage(
|
||||||
|
subscription.data[0].attributes.first_subscription_item.id,
|
||||||
|
roundedUsageTime
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importUserUsageData(run_id: string) {
|
||||||
|
const workflowRuns = await db.query.workflowRunsTable.findFirst({
|
||||||
|
where: eq(workflowRunsTable.id, run_id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!workflowRuns?.workflow_id) return;
|
||||||
|
|
||||||
|
// find if workflowTable id column contains workflowRunsTable workflow_id
|
||||||
|
const workflow = await db.query.workflowTable.findFirst({
|
||||||
|
where: eq(workflowTable.id, workflowRuns.workflow_id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (workflowRuns?.ended_at === null || workflow == null) return;
|
||||||
|
|
||||||
|
const usageTime = parseFloat(
|
||||||
|
getDuration((workflowRuns?.ended_at - workflowRuns?.started_at) / 1000)
|
||||||
|
);
|
||||||
|
|
||||||
|
// add data to userUsageTable
|
||||||
|
const user_usage = await db.insert(userUsageTable).values({
|
||||||
|
user_id: workflow.user_id,
|
||||||
|
created_at: workflowRuns.ended_at,
|
||||||
|
org_id: workflow.org_id,
|
||||||
|
ended_at: workflowRuns.ended_at,
|
||||||
|
usage_time: usageTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
return usageTime;
|
||||||
|
}
|
||||||
|
81
web/src/app/(app)/pricing/components/gpuPricingTable.tsx
Normal file
81
web/src/app/(app)/pricing/components/gpuPricingTable.tsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
const people = [
|
||||||
|
{
|
||||||
|
name: "Nvidia T4 GPU",
|
||||||
|
gpu: "1x",
|
||||||
|
ram: "16GB",
|
||||||
|
price: "$0.000225/sec",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nvidia A40 GPU",
|
||||||
|
gpu: "1x",
|
||||||
|
ram: "48GB",
|
||||||
|
price: "$0.000575/sec",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function GpuPricingPlan() {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center w-full py-8">
|
||||||
|
<div className="w-full max-w-4xl">
|
||||||
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
||||||
|
>
|
||||||
|
GPU
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 lg:table-cell"
|
||||||
|
>
|
||||||
|
No.
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="hidden px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:table-cell"
|
||||||
|
>
|
||||||
|
RAM
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Price
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200 bg-white">
|
||||||
|
{people.map((person) => (
|
||||||
|
<tr key={person.ram} className="even:bg-gray-50">
|
||||||
|
<td className="w-full max-w-0 py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:w-auto sm:max-w-none sm:pl-6">
|
||||||
|
{person.name}
|
||||||
|
<dl className="font-normal lg:hidden">
|
||||||
|
<dt className="sr-only">No.</dt>
|
||||||
|
<dd className="mt-1 truncate text-gray-700">
|
||||||
|
{person.gpu}
|
||||||
|
</dd>
|
||||||
|
<dt className="sr-only sm:hidden">RAM</dt>
|
||||||
|
<dd className="mt-1 truncate text-gray-500 sm:hidden">
|
||||||
|
{person.ram}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</td>
|
||||||
|
<td className="hidden px-3 py-4 text-sm text-gray-500 lg:table-cell">
|
||||||
|
{person.gpu}
|
||||||
|
</td>
|
||||||
|
<td className="hidden px-3 py-4 text-sm text-gray-500 sm:table-cell">
|
||||||
|
{person.ram}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-4 text-sm text-gray-500">
|
||||||
|
{person.price}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
157
web/src/app/(app)/pricing/components/pricePlanList.tsx
Normal file
157
web/src/app/(app)/pricing/components/pricePlanList.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { checkMarkIcon, crossMarkIcon } from "../const/Icon";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { getPricing } from "@/server/linkToPricing";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
type Tier = {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
href: string;
|
||||||
|
priceMonthly: string;
|
||||||
|
description: string;
|
||||||
|
features: string[];
|
||||||
|
featured: boolean;
|
||||||
|
priority?: TierPriority;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum TierPriority {
|
||||||
|
Free = "free",
|
||||||
|
Pro = "pro",
|
||||||
|
Enterprise = "enterprise",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PricingList() {
|
||||||
|
const [productTiers, setProductTiers] = useState<Tier[]>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
const product = await getPricing();
|
||||||
|
|
||||||
|
if (!product) return;
|
||||||
|
|
||||||
|
const newProductTiers: Tier[] = product.data.map((item) => {
|
||||||
|
// Create a new DOMParser instance
|
||||||
|
const parser = new DOMParser();
|
||||||
|
// Parse the description HTML string to a new document
|
||||||
|
const doc = parser.parseFromString(
|
||||||
|
item.attributes.description,
|
||||||
|
"text/html"
|
||||||
|
);
|
||||||
|
// Extract the description and features
|
||||||
|
const description = doc.querySelector("p")?.textContent || "";
|
||||||
|
const features = Array.from(doc.querySelectorAll("ul > li")).map(
|
||||||
|
(li) => li.textContent || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: item.attributes.name,
|
||||||
|
id: item.id,
|
||||||
|
href: item.attributes.buy_now_url,
|
||||||
|
priceMonthly:
|
||||||
|
item.attributes.price_formatted.split("/")[0] == "Usage-based"
|
||||||
|
? "$20.00"
|
||||||
|
: item.attributes.price_formatted.split("/")[0],
|
||||||
|
description: description,
|
||||||
|
features: features,
|
||||||
|
|
||||||
|
// if name contains pro, it's featured
|
||||||
|
featured: item.attributes.name.toLowerCase().includes("pro"),
|
||||||
|
|
||||||
|
// give priority if name contain in enum
|
||||||
|
priority: Object.values(TierPriority).find((priority) =>
|
||||||
|
item.attributes.name.toLowerCase().includes(priority)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// sort newProductTiers by priority
|
||||||
|
newProductTiers.sort((a, b) => {
|
||||||
|
if (!a.priority) return 1;
|
||||||
|
if (!b.priority) return -1;
|
||||||
|
return (
|
||||||
|
Object.values(TierPriority).indexOf(a.priority) -
|
||||||
|
Object.values(TierPriority).indexOf(b.priority)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
setProductTiers(newProductTiers);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative isolate px-6 py-24 lg:px-8">
|
||||||
|
<div className="mx-auto max-w-2xl text-center lg:max-w-4xl">
|
||||||
|
<h2 className="text-base font-semibold leading-7 text-indigo-600">
|
||||||
|
Pricing
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
|
||||||
|
The right price for you, whoever you are
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="mx-auto mt-6 max-w-2xl text-center text-lg leading-8 text-gray-600">
|
||||||
|
Qui iusto aut est earum eos quae. Eligendi est at nam aliquid ad quo
|
||||||
|
reprehenderit in aliquid fugiat dolorum voluptatibus.
|
||||||
|
</p>
|
||||||
|
<div className="mx-auto mt-16 grid max-w-lg grid-cols-1 items-center gap-y-6 sm:mt-20 sm:gap-y-0 lg:max-w-4xl lg:grid-cols-2 xl:max-w-6xl xl:grid-cols-3">
|
||||||
|
{productTiers &&
|
||||||
|
productTiers.map((tier, tierIdx) => (
|
||||||
|
<div
|
||||||
|
key={tier.id}
|
||||||
|
className={cn(
|
||||||
|
tier.featured
|
||||||
|
? "relative bg-white shadow-2xl"
|
||||||
|
: "bg-white/60 sm:mx-8 lg:mx-0",
|
||||||
|
tier.featured
|
||||||
|
? ""
|
||||||
|
: tierIdx === 0
|
||||||
|
? "rounded-t-3xl sm:rounded-b-none lg:rounded-tr-none lg:rounded-bl-3xl"
|
||||||
|
: "sm:rounded-t-none lg:rounded-tr-3xl lg:rounded-bl-none",
|
||||||
|
"rounded-3xl p-8 ring-1 ring-gray-900/10 sm:p-10"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
id={tier.id}
|
||||||
|
className="text-base font-semibold leading-7 text-indigo-600"
|
||||||
|
>
|
||||||
|
{tier.name}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-4 flex items-baseline gap-x-2">
|
||||||
|
<span className="text-5xl font-bold tracking-tight text-gray-900">
|
||||||
|
{tier.priceMonthly}
|
||||||
|
</span>
|
||||||
|
<span className="text-base text-gray-500">/month</span>
|
||||||
|
</p>
|
||||||
|
<p className="mt-6 text-base leading-7 text-gray-600">
|
||||||
|
{tier.description}
|
||||||
|
</p>
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className="mt-8 space-y-3 text-sm leading-6 text-gray-600 sm:mt-10"
|
||||||
|
>
|
||||||
|
{tier.features.map((feature) => (
|
||||||
|
<li key={feature} className="flex gap-x-3">
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
{feature.includes("[x]") ? crossMarkIcon : checkMarkIcon}
|
||||||
|
</div>
|
||||||
|
{feature.replace("[x]", "")}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<a
|
||||||
|
href={tier.href}
|
||||||
|
aria-describedby={tier.id}
|
||||||
|
className={cn(
|
||||||
|
tier.featured
|
||||||
|
? "bg-indigo-600 text-white shadow hover:bg-indigo-500"
|
||||||
|
: "text-indigo-600 ring-1 ring-inset ring-indigo-200 hover:ring-indigo-300",
|
||||||
|
"mt-8 block rounded-md py-2.5 px-3.5 text-center text-sm font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 sm:mt-10"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Get started today
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
37
web/src/app/(app)/pricing/const/Icon.tsx
Normal file
37
web/src/app/(app)/pricing/const/Icon.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
export const checkMarkIcon = (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 flex-shrink-0 text-green-500"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const crossMarkIcon = (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 48 48"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#F44336"
|
||||||
|
d="M21.5 4.5H26.501V43.5H21.5z"
|
||||||
|
transform="rotate(45.001 24 24)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="#F44336"
|
||||||
|
d="M21.5 4.5H26.5V43.501H21.5z"
|
||||||
|
transform="rotate(135.008 24 24)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
9
web/src/app/(app)/pricing/loading.tsx
Normal file
9
web/src/app/(app)/pricing/loading.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { LoadingPageWrapper } from "@/components/LoadingWrapper";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
const pathName = usePathname();
|
||||||
|
return <LoadingPageWrapper className="h-full" tag={pathName.toLowerCase()} />;
|
||||||
|
}
|
13
web/src/app/(app)/pricing/page.tsx
Normal file
13
web/src/app/(app)/pricing/page.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { GpuPricingPlan } from "@/app/(app)/pricing/components/gpuPricingTable";
|
||||||
|
import PricingList from "@/app/(app)/pricing/components/pricePlanList";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PricingList />
|
||||||
|
<GpuPricingPlan />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -22,6 +22,7 @@ import {
|
|||||||
} from "@clerk/nextjs";
|
} from "@clerk/nextjs";
|
||||||
import { Github, Menu } from "lucide-react";
|
import { Github, Menu } from "lucide-react";
|
||||||
import meta from "next-gen/config";
|
import meta from "next-gen/config";
|
||||||
|
import { useFeatureFlagEnabled } from "posthog-js/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useMediaQuery } from "usehooks-ts";
|
import { useMediaQuery } from "usehooks-ts";
|
||||||
|
|
||||||
@ -29,9 +30,13 @@ export function Navbar() {
|
|||||||
const { organization } = useOrganization();
|
const { organization } = useOrganization();
|
||||||
const _isDesktop = useMediaQuery("(min-width: 1024px)");
|
const _isDesktop = useMediaQuery("(min-width: 1024px)");
|
||||||
const [isDesktop, setIsDesktop] = useState(true);
|
const [isDesktop, setIsDesktop] = useState(true);
|
||||||
|
|
||||||
|
const pricingPlanFlagEnable = useFeatureFlagEnabled("pricing-plan");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsDesktop(_isDesktop);
|
setIsDesktop(_isDesktop);
|
||||||
}, [_isDesktop]);
|
}, [_isDesktop]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-row items-center gap-4">
|
<div className="flex flex-row items-center gap-4">
|
||||||
@ -85,6 +90,15 @@ export function Navbar() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
{isDesktop && <NavbarMenu />}
|
{isDesktop && <NavbarMenu />}
|
||||||
|
{pricingPlanFlagEnable && (
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant="link"
|
||||||
|
className="rounded-full aspect-square p-2 mr-4"
|
||||||
|
>
|
||||||
|
<a href="/pricing">Pricing</a>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="link"
|
variant="link"
|
||||||
@ -98,7 +112,11 @@ export function Navbar() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="rounded-full aspect-square p-2"
|
className="rounded-full aspect-square p-2"
|
||||||
>
|
>
|
||||||
<a target="_blank" href="https://github.com/BennyKok/comfyui-deploy" rel="noreferrer">
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://github.com/BennyKok/comfyui-deploy"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
<Github />
|
<Github />
|
||||||
</a>
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
uuid,
|
uuid,
|
||||||
|
real,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@ -334,6 +335,19 @@ export const apiKeyTable = dbSchema.table("api_keys", {
|
|||||||
updated_at: timestamp("updated_at").defaultNow().notNull(),
|
updated_at: timestamp("updated_at").defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const userUsageTable = dbSchema.table("user_usage", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom().notNull(),
|
||||||
|
org_id: text("org_id"),
|
||||||
|
user_id: text("user_id")
|
||||||
|
.references(() => usersTable.id, {
|
||||||
|
onDelete: "cascade",
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
|
usage_time: real("usage_time").default(0).notNull(),
|
||||||
|
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
ended_at: timestamp("ended_at").defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
export const authRequestsTable = dbSchema.table("auth_requests", {
|
export const authRequestsTable = dbSchema.table("auth_requests", {
|
||||||
request_id: text("request_id").primaryKey().notNull(),
|
request_id: text("request_id").primaryKey().notNull(),
|
||||||
user_id: text("user_id"),
|
user_id: text("user_id"),
|
||||||
@ -349,3 +363,4 @@ export type WorkflowType = InferSelectModel<typeof workflowTable>;
|
|||||||
export type MachineType = InferSelectModel<typeof machinesTable>;
|
export type MachineType = InferSelectModel<typeof machinesTable>;
|
||||||
export type WorkflowVersionType = InferSelectModel<typeof workflowVersionTable>;
|
export type WorkflowVersionType = InferSelectModel<typeof workflowVersionTable>;
|
||||||
export type DeploymentType = InferSelectModel<typeof deploymentsTable>;
|
export type DeploymentType = InferSelectModel<typeof deploymentsTable>;
|
||||||
|
export type UserUsageType = InferSelectModel<typeof userUsageTable>;
|
||||||
|
45
web/src/server/linkToPricing.ts
Normal file
45
web/src/server/linkToPricing.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { LemonSqueezy } from "@lemonsqueezy/lemonsqueezy.js";
|
||||||
|
import "server-only";
|
||||||
|
|
||||||
|
const ls = new LemonSqueezy(process.env.LEMONSQUEEZY_API_KEY || "");
|
||||||
|
|
||||||
|
export async function getPricing() {
|
||||||
|
const products = await ls.getProducts();
|
||||||
|
|
||||||
|
return products;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUsage() {
|
||||||
|
const usageRecord = await ls.getUsageRecords();
|
||||||
|
|
||||||
|
return usageRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setUsage(id: number, quantity: number) {
|
||||||
|
const setUsage = await ls.createUsageRecord({
|
||||||
|
subscriptionItemId: id,
|
||||||
|
quantity: quantity,
|
||||||
|
});
|
||||||
|
|
||||||
|
return setUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSubscription() {
|
||||||
|
const subscription = await ls.getSubscriptions();
|
||||||
|
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSubscriptionItem() {
|
||||||
|
const subscriptionItem = await ls.getSubscriptionItems();
|
||||||
|
|
||||||
|
return subscriptionItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserData() {
|
||||||
|
const user = await ls.getUser();
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user